feat(routing): routing_rules schema + types — M4-001/002/003 (#315)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user