feat(M3-001): refactor ProviderService into IProviderAdapter pattern
Introduces the IProviderAdapter interface as the architecture foundation for multi-provider support. Extracts Ollama logic into OllamaAdapter and makes ProviderService adapter-aware while preserving the full existing API. ## What changed ### @mosaic/types — new provider adapter types - IProviderAdapter interface: register(), listModels(), healthCheck(), createCompletion() - ProviderHealth / ProviderHealthStatus — health check result - CompletionParams / CompletionMessage / CompletionTool — completion request - CompletionEvent / CompletionUsage — streamed completion response ### apps/gateway — adapter implementation - apps/gateway/src/agent/adapters/ollama.adapter.ts — OllamaAdapter (extracted from ProviderService) - apps/gateway/src/agent/adapters/index.ts — barrel export - ProviderService.onModuleInit() is now async (awaits registerAll()) - New ProviderService methods: registerAll(), getAdapter(), healthCheckAll() - PROVIDER_ADAPTERS symbol exported as future DI injection token - Anthropic/OpenAI/Z.ai remain as direct registry calls (M3-002 to M3-005 scope) - Updated provider.service.test.ts: all tests now await onModuleInit() ## Pi SDK compatibility findings Pi SDK uses ModelRegistry as central registry. The adapter pattern is a Mosaic abstraction layered on top — adapters call registry.registerProvider() during register(). Pi SDK has no native adapter concept; it does not conflict. createCompletion() is defined in the interface but not called by the Pi layer. Pi SDK's AgentSession.prompt() and ModelRegistry.getAvailable() handle all actual completions. createCompletion() is reserved for future direct-completion use cases (post-M3 scope). OllamaAdapter throws NotImplementedError for now. No Pi SDK friction was found for the adapter pattern. The registry integration point (registerProvider()) is exactly what the existing code already used. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,9 @@ describe('ProviderService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('skips API-key providers when env vars are missing (no models become available)', () => {
|
||||
it('skips API-key providers when env vars are missing (no models become available)', async () => {
|
||||
const service = new ProviderService();
|
||||
service.onModuleInit();
|
||||
await service.onModuleInit();
|
||||
|
||||
// Pi's built-in registry may include model definitions for all providers, but
|
||||
// without API keys none of them should be available (usable).
|
||||
@@ -54,11 +54,11 @@ describe('ProviderService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('registers Anthropic provider with correct models when ANTHROPIC_API_KEY is set', () => {
|
||||
it('registers Anthropic provider with correct models when ANTHROPIC_API_KEY is set', async () => {
|
||||
process.env['ANTHROPIC_API_KEY'] = 'test-anthropic';
|
||||
|
||||
const service = new ProviderService();
|
||||
service.onModuleInit();
|
||||
await service.onModuleInit();
|
||||
|
||||
const providers = service.listProviders();
|
||||
const anthropic = providers.find((p) => p.id === 'anthropic');
|
||||
@@ -77,11 +77,11 @@ describe('ProviderService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('registers OpenAI provider with correct models when OPENAI_API_KEY is set', () => {
|
||||
it('registers OpenAI provider with correct models when OPENAI_API_KEY is set', async () => {
|
||||
process.env['OPENAI_API_KEY'] = 'test-openai';
|
||||
|
||||
const service = new ProviderService();
|
||||
service.onModuleInit();
|
||||
await service.onModuleInit();
|
||||
|
||||
const providers = service.listProviders();
|
||||
const openai = providers.find((p) => p.id === 'openai');
|
||||
@@ -90,11 +90,11 @@ describe('ProviderService', () => {
|
||||
expect(openai!.models.map((m) => m.id)).toEqual(['gpt-4o', 'gpt-4o-mini', 'o3-mini']);
|
||||
});
|
||||
|
||||
it('registers Z.ai provider with correct models when ZAI_API_KEY is set', () => {
|
||||
it('registers Z.ai provider with correct models when ZAI_API_KEY is set', async () => {
|
||||
process.env['ZAI_API_KEY'] = 'test-zai';
|
||||
|
||||
const service = new ProviderService();
|
||||
service.onModuleInit();
|
||||
await service.onModuleInit();
|
||||
|
||||
const providers = service.listProviders();
|
||||
const zai = providers.find((p) => p.id === 'zai');
|
||||
@@ -103,13 +103,13 @@ describe('ProviderService', () => {
|
||||
expect(zai!.models.map((m) => m.id)).toEqual(['glm-4.5', 'glm-4.5-air', 'glm-4.5-flash']);
|
||||
});
|
||||
|
||||
it('registers all three providers when all keys are set', () => {
|
||||
it('registers all three providers when all keys are set', async () => {
|
||||
process.env['ANTHROPIC_API_KEY'] = 'test-anthropic';
|
||||
process.env['OPENAI_API_KEY'] = 'test-openai';
|
||||
process.env['ZAI_API_KEY'] = 'test-zai';
|
||||
|
||||
const service = new ProviderService();
|
||||
service.onModuleInit();
|
||||
await service.onModuleInit();
|
||||
|
||||
const providerIds = service.listProviders().map((p) => p.id);
|
||||
expect(providerIds).toContain('anthropic');
|
||||
@@ -117,11 +117,11 @@ describe('ProviderService', () => {
|
||||
expect(providerIds).toContain('zai');
|
||||
});
|
||||
|
||||
it('can find registered Anthropic models by provider+id', () => {
|
||||
it('can find registered Anthropic models by provider+id', async () => {
|
||||
process.env['ANTHROPIC_API_KEY'] = 'test-anthropic';
|
||||
|
||||
const service = new ProviderService();
|
||||
service.onModuleInit();
|
||||
await service.onModuleInit();
|
||||
|
||||
const sonnet = service.findModel('anthropic', 'claude-sonnet-4-6');
|
||||
expect(sonnet).toBeDefined();
|
||||
@@ -129,11 +129,11 @@ describe('ProviderService', () => {
|
||||
expect(sonnet!.id).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('can find registered Z.ai models by provider+id', () => {
|
||||
it('can find registered Z.ai models by provider+id', async () => {
|
||||
process.env['ZAI_API_KEY'] = 'test-zai';
|
||||
|
||||
const service = new ProviderService();
|
||||
service.onModuleInit();
|
||||
await service.onModuleInit();
|
||||
|
||||
const glm = service.findModel('zai', 'glm-4.5');
|
||||
expect(glm).toBeDefined();
|
||||
|
||||
1
apps/gateway/src/agent/adapters/index.ts
Normal file
1
apps/gateway/src/agent/adapters/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { OllamaAdapter } from './ollama.adapter.js';
|
||||
125
apps/gateway/src/agent/adapters/ollama.adapter.ts
Normal file
125
apps/gateway/src/agent/adapters/ollama.adapter.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { ModelRegistry } from '@mariozechner/pi-coding-agent';
|
||||
import type {
|
||||
CompletionEvent,
|
||||
CompletionParams,
|
||||
IProviderAdapter,
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
} from '@mosaic/types';
|
||||
|
||||
/**
|
||||
* Ollama provider adapter.
|
||||
*
|
||||
* Registers local Ollama models with the Pi ModelRegistry via the OpenAI-compatible
|
||||
* completions API. Configuration is driven by environment variables:
|
||||
* OLLAMA_BASE_URL or OLLAMA_HOST — base URL of the Ollama instance
|
||||
* OLLAMA_MODELS — comma-separated list of model IDs (default: llama3.2,codellama,mistral)
|
||||
*/
|
||||
export class OllamaAdapter implements IProviderAdapter {
|
||||
readonly name = 'ollama';
|
||||
|
||||
private readonly logger = new Logger(OllamaAdapter.name);
|
||||
private registeredModels: ModelInfo[] = [];
|
||||
|
||||
constructor(private readonly registry: ModelRegistry) {}
|
||||
|
||||
async register(): Promise<void> {
|
||||
const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST'];
|
||||
if (!ollamaUrl) {
|
||||
this.logger.debug('Skipping Ollama provider registration: OLLAMA_BASE_URL not set');
|
||||
return;
|
||||
}
|
||||
|
||||
const modelsEnv = process.env['OLLAMA_MODELS'] ?? 'llama3.2,codellama,mistral';
|
||||
const modelIds = modelsEnv
|
||||
.split(',')
|
||||
.map((id: string) => id.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
this.registry.registerProvider('ollama', {
|
||||
baseUrl: `${ollamaUrl}/v1`,
|
||||
apiKey: 'ollama',
|
||||
api: 'openai-completions' as never,
|
||||
models: modelIds.map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
reasoning: false,
|
||||
input: ['text'] as ('text' | 'image')[],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 4096,
|
||||
})),
|
||||
});
|
||||
|
||||
this.registeredModels = modelIds.map((id) => ({
|
||||
id,
|
||||
provider: 'ollama',
|
||||
name: id,
|
||||
reasoning: false,
|
||||
contextWindow: 8192,
|
||||
maxTokens: 4096,
|
||||
inputTypes: ['text'] as ('text' | 'image')[],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
}));
|
||||
|
||||
this.logger.log(
|
||||
`Ollama provider registered at ${ollamaUrl} with models: ${modelIds.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
listModels(): ModelInfo[] {
|
||||
return this.registeredModels;
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<ProviderHealth> {
|
||||
const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST'];
|
||||
if (!ollamaUrl) {
|
||||
return {
|
||||
status: 'down',
|
||||
lastChecked: new Date().toISOString(),
|
||||
error: 'OLLAMA_BASE_URL not configured',
|
||||
};
|
||||
}
|
||||
|
||||
const checkUrl = `${ollamaUrl}/v1/models`;
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const res = await fetch(checkUrl, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
const latencyMs = Date.now() - start;
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: 'degraded',
|
||||
latencyMs,
|
||||
lastChecked: new Date().toISOString(),
|
||||
error: `HTTP ${res.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 'healthy', latencyMs, lastChecked: new Date().toISOString() };
|
||||
} catch (err) {
|
||||
const latencyMs = Date.now() - start;
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
return { status: 'down', latencyMs, lastChecked: new Date().toISOString(), error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createCompletion is reserved for future direct-completion use.
|
||||
* The current integration routes completions through Pi SDK's ModelRegistry/AgentSession.
|
||||
*/
|
||||
async *createCompletion(_params: CompletionParams): AsyncIterable<CompletionEvent> {
|
||||
throw new Error(
|
||||
'OllamaAdapter.createCompletion is not yet implemented. ' +
|
||||
'Use Pi SDK AgentSession for completions.',
|
||||
);
|
||||
// Satisfy the AsyncGenerator return type — unreachable but required for TypeScript.
|
||||
yield undefined as never;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,43 @@
|
||||
import { Injectable, Logger, type OnModuleInit } from '@nestjs/common';
|
||||
import { ModelRegistry, AuthStorage } from '@mariozechner/pi-coding-agent';
|
||||
import { getModel, type Model, type Api } from '@mariozechner/pi-ai';
|
||||
import type { ModelInfo, ProviderInfo, CustomProviderConfig } from '@mosaic/types';
|
||||
import type {
|
||||
CustomProviderConfig,
|
||||
IProviderAdapter,
|
||||
ModelInfo,
|
||||
ProviderHealth,
|
||||
ProviderInfo,
|
||||
} from '@mosaic/types';
|
||||
import { OllamaAdapter } from './adapters/index.js';
|
||||
import type { TestConnectionResultDto } from './provider.dto.js';
|
||||
|
||||
/** DI injection token for the provider adapter array. */
|
||||
export const PROVIDER_ADAPTERS = Symbol('PROVIDER_ADAPTERS');
|
||||
|
||||
@Injectable()
|
||||
export class ProviderService implements OnModuleInit {
|
||||
private readonly logger = new Logger(ProviderService.name);
|
||||
private registry!: ModelRegistry;
|
||||
|
||||
onModuleInit(): void {
|
||||
/**
|
||||
* Adapters registered with this service.
|
||||
* Built-in adapters (Ollama) are always present; additional adapters can be
|
||||
* supplied via the PROVIDER_ADAPTERS injection token in the future.
|
||||
*/
|
||||
private adapters: IProviderAdapter[] = [];
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
this.registry = new ModelRegistry(authStorage);
|
||||
|
||||
this.registerOllamaProvider();
|
||||
// Build the default set of adapters that rely on the registry
|
||||
this.adapters = [new OllamaAdapter(this.registry)];
|
||||
|
||||
// Run all adapter registrations first (Ollama, and any future adapters)
|
||||
await this.registerAll();
|
||||
|
||||
// Register API-key providers directly (Anthropic, OpenAI, Z.ai, custom)
|
||||
// These do not yet have dedicated adapter classes (M3-002 through M3-005).
|
||||
this.registerAnthropicProvider();
|
||||
this.registerOpenAIProvider();
|
||||
this.registerZaiProvider();
|
||||
@@ -23,6 +47,59 @@ export class ProviderService implements OnModuleInit {
|
||||
this.logger.log(`Providers initialized: ${available.length} models available`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adapter-pattern API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Call register() on each adapter in order.
|
||||
* Errors from individual adapters are logged and do not abort the others.
|
||||
*/
|
||||
async registerAll(): Promise<void> {
|
||||
for (const adapter of this.adapters) {
|
||||
try {
|
||||
await adapter.register();
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Adapter "${adapter.name}" registration failed`,
|
||||
err instanceof Error ? err.stack : String(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the adapter registered under the given provider name, or undefined.
|
||||
*/
|
||||
getAdapter(providerName: string): IProviderAdapter | undefined {
|
||||
return this.adapters.find((a) => a.name === providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run healthCheck() on all adapters and return results keyed by provider name.
|
||||
*/
|
||||
async healthCheckAll(): Promise<Record<string, ProviderHealth>> {
|
||||
const results: Record<string, ProviderHealth> = {};
|
||||
await Promise.all(
|
||||
this.adapters.map(async (adapter) => {
|
||||
try {
|
||||
results[adapter.name] = await adapter.healthCheck();
|
||||
} catch (err) {
|
||||
results[adapter.name] = {
|
||||
status: 'down',
|
||||
lastChecked: new Date().toISOString(),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Legacy / Pi-SDK-facing API (preserved for AgentService and RoutingService)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getRegistry(): ModelRegistry {
|
||||
return this.registry;
|
||||
}
|
||||
@@ -69,6 +146,18 @@ export class ProviderService implements OnModuleInit {
|
||||
}
|
||||
|
||||
async testConnection(providerId: string, baseUrl?: string): Promise<TestConnectionResultDto> {
|
||||
// Delegate to the adapter when one exists and no URL override is given
|
||||
const adapter = this.getAdapter(providerId);
|
||||
if (adapter && !baseUrl) {
|
||||
const health = await adapter.healthCheck();
|
||||
return {
|
||||
providerId,
|
||||
reachable: health.status !== 'down',
|
||||
latencyMs: health.latencyMs,
|
||||
error: health.error,
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve baseUrl: explicit override > registered provider > ollama env
|
||||
let resolvedUrl = baseUrl;
|
||||
|
||||
@@ -143,6 +232,11 @@ export class ProviderService implements OnModuleInit {
|
||||
this.logger.log(`Registered custom provider: ${config.id} (${config.models.length} models)`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers — direct registry registration for providers without adapters yet
|
||||
// (Anthropic, OpenAI, Z.ai will move to adapters in M3-002 through M3-005)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private registerAnthropicProvider(): void {
|
||||
const apiKey = process.env['ANTHROPIC_API_KEY'];
|
||||
if (!apiKey) {
|
||||
@@ -203,36 +297,6 @@ export class ProviderService implements OnModuleInit {
|
||||
this.logger.log('Z.ai provider registered with 3 models');
|
||||
}
|
||||
|
||||
private registerOllamaProvider(): void {
|
||||
const ollamaUrl = process.env['OLLAMA_BASE_URL'] ?? process.env['OLLAMA_HOST'];
|
||||
if (!ollamaUrl) return;
|
||||
|
||||
const modelsEnv = process.env['OLLAMA_MODELS'] ?? 'llama3.2,codellama,mistral';
|
||||
const modelIds = modelsEnv
|
||||
.split(',')
|
||||
.map((modelId: string) => modelId.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
this.registry.registerProvider('ollama', {
|
||||
baseUrl: `${ollamaUrl}/v1`,
|
||||
apiKey: 'ollama',
|
||||
api: 'openai-completions' as never,
|
||||
models: modelIds.map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
reasoning: false,
|
||||
input: ['text'] as ('text' | 'image')[],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 4096,
|
||||
})),
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Ollama provider registered at ${ollamaUrl} with models: ${modelIds.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
private registerCustomProviders(): void {
|
||||
const customJson = process.env['MOSAIC_CUSTOM_PROVIDERS'];
|
||||
if (!customJson) return;
|
||||
|
||||
55
docs/scratchpads/m3-001-provider-adapter.md
Normal file
55
docs/scratchpads/m3-001-provider-adapter.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# M3-001 Provider Adapter Pattern — Scratchpad
|
||||
|
||||
## Objective
|
||||
|
||||
Refactor ProviderService into an IProviderAdapter pattern without breaking existing Ollama flow.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add `IProviderAdapter` interface and supporting types to `@mosaic/types` provider package
|
||||
2. Create `apps/gateway/src/agent/adapters/` directory with:
|
||||
- `provider-adapter.interface.ts` — IProviderAdapter + ProviderHealth + CompletionParams + CompletionEvent
|
||||
- `ollama.adapter.ts` — extract existing Ollama logic
|
||||
3. Refactor ProviderService:
|
||||
- Accept `IProviderAdapter[]` (injected via DI token)
|
||||
- `registerAll()` / `listModels()` aggregates from all adapters
|
||||
- `getAdapter(name)` — lookup by name
|
||||
- `healthCheckAll()` — check all adapters
|
||||
- Keep Pi ModelRegistry wiring (required by AgentService)
|
||||
4. Wire up in AgentModule
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Pi SDK Compatibility
|
||||
|
||||
- Pi SDK uses `ModelRegistry` as central registry; ProviderService wraps it
|
||||
- `ModelRegistry.registerProvider()` is the integration point — adapters call this
|
||||
- Pi doesn't have a native "IProviderAdapter" concept — adapters are a Mosaic abstraction on top
|
||||
- The `createAgentSession()` call in AgentService uses `modelRegistry: this.providerService.getRegistry()`
|
||||
- OllamaAdapter should call `registry.registerProvider('ollama', {...})` same as today
|
||||
- CompletionParams/CompletionEvent: Pi SDK streams via `AgentSession.prompt()`, not raw completion
|
||||
— IProviderAdapter.createCompletion() is for future direct use; for now stub or leave as interface-only
|
||||
— ASSUMPTION: createCompletion is reserved for future M3+ work; Pi SDK owns the actual streaming
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- ESM: use `.js` extensions in all imports
|
||||
- NestJS: use `@Inject()` explicitly
|
||||
- Keep RoutingService working — it only uses `providerService.listAvailableModels()`
|
||||
- Keep AgentService working — it uses `providerService.getRegistry()`, `findModel()`, `getDefaultModel()`, `listAvailableModels()`
|
||||
|
||||
## Progress
|
||||
|
||||
- [ ] Add types to @mosaic/types
|
||||
- [ ] Create adapters/ directory
|
||||
- [ ] Create IProviderAdapter interface file
|
||||
- [ ] Create OllamaAdapter
|
||||
- [ ] Refactor ProviderService
|
||||
- [ ] Update AgentModule
|
||||
- [ ] Run tests
|
||||
- [ ] Run quality gates
|
||||
|
||||
## Risks
|
||||
|
||||
- Pi SDK doesn't natively support IProviderAdapter — adapters are a layer on top
|
||||
- createCompletion() is architecturally sound but requires Pi session bypass (future work)
|
||||
@@ -52,3 +52,100 @@ export interface CustomProviderConfig {
|
||||
maxTokens?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IProviderAdapter pattern — M3-001
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Health status of a provider */
|
||||
export type ProviderHealthStatus = 'healthy' | 'degraded' | 'down';
|
||||
|
||||
/** Result of a provider health check */
|
||||
export interface ProviderHealth {
|
||||
status: ProviderHealthStatus;
|
||||
/** Round-trip latency in milliseconds (undefined when provider is down) */
|
||||
latencyMs?: number;
|
||||
/** ISO-8601 timestamp of the check */
|
||||
lastChecked: string;
|
||||
/** Human-readable error message (defined when status is not healthy) */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** A single message in a completion request */
|
||||
export interface CompletionMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Tool definition for completion requests */
|
||||
export interface CompletionTool {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Parameters for a completion request */
|
||||
export interface CompletionParams {
|
||||
model: string;
|
||||
messages: CompletionMessage[];
|
||||
tools?: CompletionTool[];
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
stream?: boolean;
|
||||
}
|
||||
|
||||
/** Usage statistics for a completion event */
|
||||
export interface CompletionUsage {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
}
|
||||
|
||||
/** A streamed completion event */
|
||||
export type CompletionEvent =
|
||||
| { type: 'text_delta'; content: string }
|
||||
| { type: 'tool_call'; name: string; arguments: string }
|
||||
| { type: 'done'; usage?: CompletionUsage };
|
||||
|
||||
/**
|
||||
* Pluggable provider adapter interface.
|
||||
*
|
||||
* Each LLM provider (Anthropic, OpenAI, Ollama, etc.) implements this interface
|
||||
* to integrate with Mosaic's provider layer. The ProviderService aggregates all
|
||||
* registered adapters and routes requests accordingly.
|
||||
*
|
||||
* Note on createCompletion: this method is part of the interface for future
|
||||
* direct-completion use cases. The current Pi SDK integration routes completions
|
||||
* through the Pi session/ModelRegistry layer rather than calling adapters directly.
|
||||
* Adapters MUST still implement register() and healthCheck() correctly — those are
|
||||
* used by ProviderService today.
|
||||
*/
|
||||
export interface IProviderAdapter {
|
||||
/** Unique provider identifier (e.g. 'anthropic', 'openai', 'ollama') */
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Initialize the provider — connect, discover models, register with the
|
||||
* Pi ModelRegistry. Called once at module startup by ProviderService.registerAll().
|
||||
*/
|
||||
register(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return the list of models this adapter makes available.
|
||||
* Returns an empty array when the provider is not configured.
|
||||
*/
|
||||
listModels(): ModelInfo[];
|
||||
|
||||
/**
|
||||
* Check whether the provider endpoint is reachable and responsive.
|
||||
*/
|
||||
healthCheck(): Promise<ProviderHealth>;
|
||||
|
||||
/**
|
||||
* Stream a completion from the provider.
|
||||
*
|
||||
* Note: Currently reserved for future use. The Pi SDK integration routes
|
||||
* completions through ModelRegistry / AgentSession rather than this method.
|
||||
* Implementations may throw NotImplementedError until M3+ tasks wire this up.
|
||||
*/
|
||||
createCompletion(params: CompletionParams): AsyncIterable<CompletionEvent>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user