feat(routing): routing_rules schema + types — M4-001/002/003 (#315)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #315.
This commit is contained in:
2026-03-23 00:08:56 +00:00
committed by jason.woltje
parent cabd39ba5b
commit 0ee6bfe9de
14 changed files with 3466 additions and 42 deletions

View File

@@ -1,4 +1,11 @@
import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';
import {
Inject,
Injectable,
Logger,
Optional,
type OnModuleDestroy,
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 {
@@ -13,8 +20,10 @@ import {
OllamaAdapter,
OpenAIAdapter,
OpenRouterAdapter,
ZaiAdapter,
} from './adapters/index.js';
import type { TestConnectionResultDto } from './provider.dto.js';
import { ProviderCredentialsService } from './provider-credentials.service.js';
/** Default health check interval in seconds */
const DEFAULT_HEALTH_INTERVAL_SECS = 60;
@@ -22,11 +31,25 @@ const DEFAULT_HEALTH_INTERVAL_SECS = 60;
/** DI injection token for the provider adapter array. */
export const PROVIDER_ADAPTERS = Symbol('PROVIDER_ADAPTERS');
/** Environment variable names for well-known providers */
const PROVIDER_ENV_KEYS: Record<string, string> = {
anthropic: 'ANTHROPIC_API_KEY',
openai: 'OPENAI_API_KEY',
openrouter: 'OPENROUTER_API_KEY',
zai: 'ZAI_API_KEY',
};
@Injectable()
export class ProviderService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(ProviderService.name);
private registry!: ModelRegistry;
constructor(
@Optional()
@Inject(ProviderCredentialsService)
private readonly credentialsService: ProviderCredentialsService | null,
) {}
/**
* Adapters registered with this service.
* Built-in adapters (Ollama) are always present; additional adapters can be
@@ -52,14 +75,13 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
new AnthropicAdapter(this.registry),
new OpenAIAdapter(this.registry),
new OpenRouterAdapter(),
new ZaiAdapter(),
];
// Run all adapter registrations first (Ollama, Anthropic, and any future adapters)
// Run all adapter registrations first (Ollama, Anthropic, OpenAI, OpenRouter, Z.ai)
await this.registerAll();
// Register API-key providers directly (Z.ai, custom)
// OpenAI now has a dedicated adapter (M3-003).
this.registerZaiProvider();
// Register API-key providers directly (custom)
this.registerCustomProviders();
const available = this.registry.getAvailable();
@@ -340,30 +362,9 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
}
// ---------------------------------------------------------------------------
// Private helpers — direct registry registration for providers without adapters yet
// (Z.ai will move to an adapter in M3-005)
// Private helpers
// ---------------------------------------------------------------------------
private registerZaiProvider(): void {
const apiKey = process.env['ZAI_API_KEY'];
if (!apiKey) {
this.logger.debug('Skipping Z.ai provider registration: ZAI_API_KEY not set');
return;
}
const models = ['glm-4.5', 'glm-4.5-air', 'glm-4.5-flash'].map((id) =>
this.cloneBuiltInModel('zai', id),
);
this.registry.registerProvider('zai', {
apiKey,
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
models,
});
this.logger.log('Z.ai provider registered with 3 models');
}
private registerCustomProviders(): void {
const customJson = process.env['MOSAIC_CUSTOM_PROVIDERS'];
if (!customJson) return;
@@ -378,6 +379,29 @@ export class ProviderService implements OnModuleInit, OnModuleDestroy {
}
}
/**
* Resolve an API key for a provider, scoped to a specific user.
* User-stored credentials take precedence over environment variables.
* Returns null if no key is available from either source.
*/
async resolveApiKey(userId: string, provider: string): Promise<string | null> {
if (this.credentialsService) {
const userKey = await this.credentialsService.retrieve(userId, provider);
if (userKey) {
this.logger.debug(`Using user-scoped credential for user=${userId} provider=${provider}`);
return userKey;
}
}
// Fall back to environment variable
const envVar = PROVIDER_ENV_KEYS[provider];
const envKey = envVar ? (process.env[envVar] ?? null) : null;
if (envKey) {
this.logger.debug(`Using env-var credential for provider=${provider}`);
}
return envKey;
}
private cloneBuiltInModel(
provider: string,
modelId: string,