mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 05:05:52 +08:00
feat(server): converge legacy compatibility (#15426)
#### PR Dependency Tree * **PR #15426** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added workspace BYOK profiles with provider/model catalogs, capability validation, connection probing, credential rotation, reordering, and secure local leases. * Added Copilot route options, selectable targets, managed tiers, explicit profile/model overrides, and improved streaming with tool callbacks and abort support. * Added Copilot availability controls to prevent access when the feature is disabled. * **Changes** * Simplified Copilot configuration and removed legacy provider-specific settings. * Removed obsolete model, token-cost, transcript strategy, and provider metadata fields from public responses. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,100 +0,0 @@
|
||||
import { CopilotProviderSideError, UserFriendlyError } from '../../../../base';
|
||||
import {
|
||||
type LlmBackendConfig,
|
||||
llmResolveRequestIntentOptions,
|
||||
} from '../../../../native';
|
||||
import { CopilotProvider } from '../provider';
|
||||
import { hasProviderModelBehaviorFlag } from '../provider-model-runtime';
|
||||
import {
|
||||
type CopilotProviderExecution,
|
||||
type ProviderDriverSpec,
|
||||
} from '../provider-runtime-contract';
|
||||
import { CopilotProviderType } from '../types';
|
||||
import {
|
||||
getGoogleAuth,
|
||||
getVertexAnthropicBaseUrl,
|
||||
type VertexAnthropicProviderConfig,
|
||||
} from '../utils';
|
||||
|
||||
export abstract class AnthropicProvider<T> extends CopilotProvider<T> {
|
||||
protected resolveModelBackendKind() {
|
||||
return this.type === CopilotProviderType.AnthropicVertex
|
||||
? ('anthropic_vertex' as const)
|
||||
: ('anthropic' as const);
|
||||
}
|
||||
|
||||
override getDriverSpec(): ProviderDriverSpec {
|
||||
return {
|
||||
createBackendConfig: execution => this.createNativeConfig(execution),
|
||||
mapError: error => this.handleError(error),
|
||||
chat: {
|
||||
resolveRequestOptions: async context => {
|
||||
const requestIntent = await llmResolveRequestIntentOptions({
|
||||
protocol: context.protocol,
|
||||
backendConfig: context.backendConfig,
|
||||
reasoning: {
|
||||
enabled: context.options.reasoning,
|
||||
supported: hasProviderModelBehaviorFlag(
|
||||
context.model,
|
||||
'reasoning_budget_12000'
|
||||
),
|
||||
budgetTokens: hasProviderModelBehaviorFlag(
|
||||
context.model,
|
||||
'reasoning_budget_12000'
|
||||
)
|
||||
? 12000
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
attachmentCapability: this.getAttachCapability(
|
||||
context.model,
|
||||
context.outputType
|
||||
),
|
||||
reasoning: requestIntent.reasoning,
|
||||
};
|
||||
},
|
||||
},
|
||||
structured: false,
|
||||
embedding: false,
|
||||
rerank: false,
|
||||
};
|
||||
}
|
||||
|
||||
private handleError(e: any) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
return e;
|
||||
}
|
||||
return new CopilotProviderSideError({
|
||||
provider: this.type,
|
||||
kind: 'unexpected_response',
|
||||
message: e?.message || 'Unexpected anthropic response',
|
||||
});
|
||||
}
|
||||
|
||||
private async createNativeConfig(
|
||||
execution?: CopilotProviderExecution
|
||||
): Promise<LlmBackendConfig> {
|
||||
const config = this.getConfig(execution);
|
||||
if (this.type === CopilotProviderType.AnthropicVertex) {
|
||||
const vertexConfig = config as VertexAnthropicProviderConfig;
|
||||
const auth = await getGoogleAuth(vertexConfig, 'anthropic');
|
||||
const { Authorization: authHeader } = auth.headers();
|
||||
const token = authHeader.replace(/^Bearer\s+/i, '');
|
||||
const baseUrl = getVertexAnthropicBaseUrl(vertexConfig) || auth.baseUrl;
|
||||
return {
|
||||
base_url: baseUrl || '',
|
||||
auth_token: token,
|
||||
headers: { Authorization: authHeader },
|
||||
};
|
||||
}
|
||||
|
||||
const officialConfig = config as { apiKey: string; baseURL?: string };
|
||||
const baseUrl = officialConfig.baseURL || 'https://api.anthropic.com/v1';
|
||||
return {
|
||||
base_url: baseUrl.replace(/\/v1\/?$/, ''),
|
||||
auth_token: officialConfig.apiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './official';
|
||||
export * from './vertex';
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { CopilotProviderExecution } from '../provider-runtime-contract';
|
||||
import { CopilotProviderType } from '../types';
|
||||
import { AnthropicProvider } from './anthropic';
|
||||
|
||||
export type AnthropicOfficialConfig = {
|
||||
apiKey: string;
|
||||
baseURL?: string;
|
||||
};
|
||||
|
||||
export class AnthropicOfficialProvider extends AnthropicProvider<AnthropicOfficialConfig> {
|
||||
override readonly type = CopilotProviderType.Anthropic;
|
||||
|
||||
override configured(execution?: CopilotProviderExecution): boolean {
|
||||
return !!this.getConfig(execution).apiKey;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { CopilotProviderExecution } from '../provider-runtime-contract';
|
||||
import { CopilotProviderType } from '../types';
|
||||
import { getVertexAnthropicBaseUrl, type VertexProviderConfig } from '../utils';
|
||||
import { AnthropicProvider } from './anthropic';
|
||||
|
||||
export type AnthropicVertexConfig = VertexProviderConfig;
|
||||
|
||||
export class AnthropicVertexProvider extends AnthropicProvider<AnthropicVertexConfig> {
|
||||
override readonly type = CopilotProviderType.AnthropicVertex;
|
||||
|
||||
override configured(execution?: CopilotProviderExecution): boolean {
|
||||
const config = this.getConfig(execution);
|
||||
if (!config.location || !config.googleAuthOptions) return false;
|
||||
return !!config.project || !!getVertexAnthropicBaseUrl(config);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,4 @@
|
||||
import type {
|
||||
ModelAttachmentCapability,
|
||||
PromptAttachment,
|
||||
PromptMessage,
|
||||
} from './types';
|
||||
|
||||
export const IMAGE_ATTACHMENT_CAPABILITY: ModelAttachmentCapability = {
|
||||
kinds: ['image'],
|
||||
sourceKinds: ['url', 'data'],
|
||||
allowRemoteUrls: true,
|
||||
};
|
||||
|
||||
export const GEMINI_ATTACHMENT_CAPABILITY: ModelAttachmentCapability = {
|
||||
kinds: ['image', 'audio', 'file'],
|
||||
sourceKinds: ['url', 'data', 'bytes', 'file_handle'],
|
||||
allowRemoteUrls: true,
|
||||
};
|
||||
|
||||
export function promptAttachmentHasSource(
|
||||
attachment: PromptAttachment
|
||||
): boolean {
|
||||
if (typeof attachment === 'string') {
|
||||
return !!attachment.trim();
|
||||
}
|
||||
|
||||
if ('attachment' in attachment) {
|
||||
return !!attachment.attachment;
|
||||
}
|
||||
|
||||
switch (attachment.kind) {
|
||||
case 'url':
|
||||
return !!attachment.url;
|
||||
case 'data':
|
||||
case 'bytes':
|
||||
return !!attachment.data;
|
||||
case 'file_handle':
|
||||
return !!attachment.fileHandle;
|
||||
}
|
||||
}
|
||||
import type { PromptAttachment, PromptMessage } from './types';
|
||||
|
||||
export function applyPromptAttachmentMimeTypeHintForNative(
|
||||
attachment: PromptAttachment,
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { CopilotProviderSideError, UserFriendlyError } from '../../../base';
|
||||
import { type LlmBackendConfig } from '../../../native';
|
||||
import { CopilotProvider } from './provider';
|
||||
import {
|
||||
type CopilotProviderExecution,
|
||||
type ProviderDriverSpec,
|
||||
} from './provider-runtime-contract';
|
||||
import { CopilotProviderType } from './types';
|
||||
|
||||
export type CloudflareWorkersAIConfig = {
|
||||
apiToken: string;
|
||||
accountId?: string;
|
||||
baseURL?: string;
|
||||
};
|
||||
|
||||
export class CloudflareWorkersAIProvider extends CopilotProvider<CloudflareWorkersAIConfig> {
|
||||
override readonly type = CopilotProviderType.CloudflareWorkersAi;
|
||||
|
||||
protected resolveModelBackendKind() {
|
||||
return 'cloudflare_workers_ai' as const;
|
||||
}
|
||||
|
||||
override configured(execution?: CopilotProviderExecution): boolean {
|
||||
const config = this.getConfig(execution);
|
||||
return !!config.apiToken && (!!config.accountId || !!config.baseURL);
|
||||
}
|
||||
private handleError(e: any) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
return e;
|
||||
}
|
||||
return new CopilotProviderSideError({
|
||||
provider: this.type,
|
||||
kind: 'unexpected_response',
|
||||
message: e?.message || 'Unexpected cloudflare workers ai response',
|
||||
});
|
||||
}
|
||||
|
||||
private createNativeConfig(
|
||||
execution?: CopilotProviderExecution
|
||||
): LlmBackendConfig {
|
||||
const config = this.getConfig(execution);
|
||||
return {
|
||||
base_url: this.resolveBaseUrl(execution),
|
||||
auth_token: config.apiToken,
|
||||
};
|
||||
}
|
||||
|
||||
private resolveBaseUrl(execution?: CopilotProviderExecution) {
|
||||
const config = this.getConfig(execution);
|
||||
if (config.baseURL) {
|
||||
return config.baseURL.replace(/\/v1\/?$/, '').replace(/\/$/, '');
|
||||
}
|
||||
const accountId = config.accountId ?? '';
|
||||
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai`;
|
||||
}
|
||||
|
||||
override getDriverSpec(): ProviderDriverSpec {
|
||||
return {
|
||||
createBackendConfig: execution => this.createNativeConfig(execution),
|
||||
mapError: error => this.handleError(error),
|
||||
structured: false,
|
||||
embedding: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,527 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { CopilotQuotaExceeded } from '../../../base';
|
||||
import { ServerFeature, ServerService } from '../../../core';
|
||||
import { type CopilotAccessContext, CopilotAccessPolicy } from '../access';
|
||||
import type { RequiredStructuredOutputContract } from '../runtime/contracts';
|
||||
import { getProviderRuntimeHost } from '../runtime/provider-runtime-context';
|
||||
import type { CopilotProvider } from './provider';
|
||||
import {
|
||||
buildProviderRegistry,
|
||||
type CopilotProviderRegistry,
|
||||
type NormalizedCopilotProviderProfile,
|
||||
resolveModel,
|
||||
stripProviderPrefix,
|
||||
} from './provider-registry';
|
||||
import type {
|
||||
CopilotProviderExecution,
|
||||
PreparedNativeEmbeddingExecution,
|
||||
PreparedNativeExecution,
|
||||
PreparedNativeImageExecution,
|
||||
PreparedNativeRerankExecution,
|
||||
PreparedNativeStructuredExecution,
|
||||
} from './provider-runtime-contract';
|
||||
import { CopilotProviderRegistryService } from './registry-service';
|
||||
import {
|
||||
type CopilotChatOptions,
|
||||
type CopilotEmbeddingOptions,
|
||||
type CopilotImageOptions,
|
||||
CopilotProviderType,
|
||||
type CopilotRerankRequest,
|
||||
type CopilotStructuredOptions,
|
||||
ModelFullConditions,
|
||||
ModelOutputType,
|
||||
type PromptMessage,
|
||||
} from './types';
|
||||
|
||||
export type ResolvedCopilotProvider = {
|
||||
providerId: string;
|
||||
provider: CopilotProvider;
|
||||
execution: CopilotProviderExecution;
|
||||
profile: NormalizedCopilotProviderProfile;
|
||||
rawModelId?: string;
|
||||
modelId?: string;
|
||||
explicitProviderId?: string;
|
||||
prepared?: PreparedNativeExecution;
|
||||
preparedStructured?: PreparedNativeStructuredExecution;
|
||||
preparedEmbedding?: PreparedNativeEmbeddingExecution;
|
||||
preparedRerank?: PreparedNativeRerankExecution;
|
||||
preparedImage?: PreparedNativeImageExecution;
|
||||
};
|
||||
|
||||
type RoutePreparationResult = Partial<
|
||||
Pick<
|
||||
ResolvedCopilotProvider,
|
||||
| 'prepared'
|
||||
| 'preparedStructured'
|
||||
| 'preparedEmbedding'
|
||||
| 'preparedRerank'
|
||||
| 'preparedImage'
|
||||
| 'modelId'
|
||||
>
|
||||
>;
|
||||
|
||||
type EffectiveProviderRegistry = {
|
||||
byokRegistry: CopilotProviderRegistry;
|
||||
quotaBackedRegistry: CopilotProviderRegistry;
|
||||
quotaBackedRoutesAvailable: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CopilotProviderFactory {
|
||||
constructor(
|
||||
private readonly server: ServerService,
|
||||
private readonly registries: CopilotProviderRegistryService,
|
||||
private readonly access: CopilotAccessPolicy
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger(CopilotProviderFactory.name);
|
||||
|
||||
readonly #providers = new Map<string, CopilotProvider>();
|
||||
readonly #providerIdsByType = new Map<CopilotProviderType, Set<string>>();
|
||||
|
||||
private getRegistry() {
|
||||
return this.registries.getRegistry();
|
||||
}
|
||||
|
||||
private getProviderByProfile(
|
||||
providerId: string,
|
||||
profile: NormalizedCopilotProviderProfile
|
||||
) {
|
||||
return (
|
||||
this.#providers.get(providerId) ??
|
||||
Array.from(this.#providerIdsByType.get(profile.type) ?? [])
|
||||
.map(id => this.#providers.get(id))
|
||||
.find((provider): provider is CopilotProvider => !!provider)
|
||||
);
|
||||
}
|
||||
|
||||
private providerAvailable(
|
||||
providerId: string,
|
||||
profile: NormalizedCopilotProviderProfile
|
||||
) {
|
||||
return !!this.getProviderByProfile(providerId, profile);
|
||||
}
|
||||
|
||||
private getAvailableProviderIds(registry: CopilotProviderRegistry) {
|
||||
return Array.from(registry.profiles.entries())
|
||||
.filter(([providerId, profile]) =>
|
||||
this.providerAvailable(providerId, profile)
|
||||
)
|
||||
.map(([providerId]) => providerId);
|
||||
}
|
||||
|
||||
private getPreferredProviderIds(
|
||||
registry: CopilotProviderRegistry,
|
||||
type?: CopilotProviderType
|
||||
) {
|
||||
if (!type) return undefined;
|
||||
return registry.byType.get(type)?.filter(providerId => {
|
||||
const profile = registry.profiles.get(providerId);
|
||||
return profile ? this.providerAvailable(providerId, profile) : false;
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeCond(
|
||||
registry: CopilotProviderRegistry,
|
||||
providerId: string,
|
||||
cond: ModelFullConditions
|
||||
): ModelFullConditions {
|
||||
const modelId = stripProviderPrefix(registry, providerId, cond.modelId);
|
||||
return { ...cond, modelId };
|
||||
}
|
||||
|
||||
private async getEffectiveRegistry(
|
||||
context: CopilotAccessContext = {}
|
||||
): Promise<EffectiveProviderRegistry> {
|
||||
const quotaBackedRegistry = this.getRegistry();
|
||||
const routeAccess = await this.access.resolveRouteAccess(context);
|
||||
|
||||
return {
|
||||
byokRegistry: buildProviderRegistry({
|
||||
profiles: routeAccess.byokProfiles,
|
||||
defaults: {},
|
||||
}),
|
||||
quotaBackedRegistry,
|
||||
quotaBackedRoutesAvailable: routeAccess.quotaBackedRoutesAvailable,
|
||||
};
|
||||
}
|
||||
|
||||
private getRequestContext(
|
||||
options?:
|
||||
| CopilotChatOptions
|
||||
| CopilotStructuredOptions
|
||||
| CopilotImageOptions
|
||||
): CopilotAccessContext {
|
||||
return {
|
||||
userId: options?.user,
|
||||
workspaceId: options?.workspace,
|
||||
byokLeaseId: options?.byokLeaseId,
|
||||
featureKind: options?.featureKind,
|
||||
quotaBackedRoutesAllowed: options?.quotaBackedRoutesAllowed,
|
||||
};
|
||||
}
|
||||
|
||||
private filterPreparedRoutes(routes: Array<ResolvedCopilotProvider | null>) {
|
||||
return routes.filter(
|
||||
(route): route is ResolvedCopilotProvider => route !== null
|
||||
);
|
||||
}
|
||||
|
||||
private async prepareResolvedRoutes(
|
||||
routes: ResolvedCopilotProvider[],
|
||||
prepare: (
|
||||
route: ResolvedCopilotProvider
|
||||
) => Promise<RoutePreparationResult | null | undefined>
|
||||
) {
|
||||
const preparedRoutes = await Promise.all(
|
||||
routes.map(async route => {
|
||||
const prepared = await prepare(route);
|
||||
return prepared ? { ...route, ...prepared } : null;
|
||||
})
|
||||
);
|
||||
|
||||
return this.filterPreparedRoutes(preparedRoutes);
|
||||
}
|
||||
|
||||
async resolveProvider(
|
||||
cond: ModelFullConditions,
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {},
|
||||
context: CopilotAccessContext = {}
|
||||
): Promise<ResolvedCopilotProvider | null> {
|
||||
return (await this.resolveRoutes(cond, filter, context))[0] ?? null;
|
||||
}
|
||||
|
||||
async resolveRoutes(
|
||||
cond: ModelFullConditions,
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {},
|
||||
context: CopilotAccessContext = {}
|
||||
): Promise<ResolvedCopilotProvider[]> {
|
||||
this.logger.debug(
|
||||
`Resolving copilot provider for output type: ${cond.outputType}`
|
||||
);
|
||||
const { byokRegistry, quotaBackedRegistry, quotaBackedRoutesAvailable } =
|
||||
await this.getEffectiveRegistry(context);
|
||||
const byokRoutes = await this.resolveRoutesFromRegistry(
|
||||
byokRegistry,
|
||||
cond,
|
||||
filter
|
||||
);
|
||||
const resolved = byokRoutes.length
|
||||
? byokRoutes
|
||||
: quotaBackedRoutesAvailable
|
||||
? await this.resolveRoutesFromRegistry(
|
||||
quotaBackedRegistry,
|
||||
cond,
|
||||
filter
|
||||
)
|
||||
: [];
|
||||
for (const route of resolved) {
|
||||
this.logger.debug(
|
||||
`Copilot provider candidate found: ${route.provider.type} (${route.providerId})`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!resolved.length &&
|
||||
!quotaBackedRoutesAvailable &&
|
||||
context.quotaBackedRoutesAllowed !== false
|
||||
) {
|
||||
const quotaBackedRoutes = await this.resolveRoutesFromRegistry(
|
||||
quotaBackedRegistry,
|
||||
cond,
|
||||
filter
|
||||
);
|
||||
if (quotaBackedRoutes.length) {
|
||||
throw new CopilotQuotaExceeded();
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private async resolveRoutesFromRegistry(
|
||||
registry: CopilotProviderRegistry,
|
||||
cond: ModelFullConditions,
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {}
|
||||
): Promise<ResolvedCopilotProvider[]> {
|
||||
const route = resolveModel({
|
||||
registry,
|
||||
modelId: cond.modelId,
|
||||
outputType: cond.outputType,
|
||||
availableProviderIds: this.getAvailableProviderIds(registry),
|
||||
preferredProviderIds: this.getPreferredProviderIds(
|
||||
registry,
|
||||
filter.prefer
|
||||
),
|
||||
});
|
||||
|
||||
const resolved: ResolvedCopilotProvider[] = [];
|
||||
for (const providerId of route.candidateProviderIds) {
|
||||
const profile = registry.profiles.get(providerId);
|
||||
const provider = profile
|
||||
? this.getProviderByProfile(providerId, profile)
|
||||
: undefined;
|
||||
if (!provider || !profile) continue;
|
||||
|
||||
const normalizedCond = this.normalizeCond(registry, providerId, cond);
|
||||
if (
|
||||
normalizedCond.modelId &&
|
||||
profile.models?.length &&
|
||||
!profile.models.includes(normalizedCond.modelId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const execution = { providerId, profile };
|
||||
const matched = await provider.match(normalizedCond, execution);
|
||||
if (!matched) continue;
|
||||
|
||||
resolved.push({
|
||||
providerId,
|
||||
provider,
|
||||
execution,
|
||||
profile,
|
||||
rawModelId: route.rawModelId,
|
||||
modelId: normalizedCond.modelId,
|
||||
explicitProviderId: route.explicitProviderId,
|
||||
});
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async prepareRoutes(
|
||||
kind: 'text' | 'streamText' | 'streamObject',
|
||||
cond: ModelFullConditions,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotChatOptions = {},
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {}
|
||||
): Promise<ResolvedCopilotProvider[]> {
|
||||
const routes = await this.resolveRoutes(
|
||||
cond,
|
||||
filter,
|
||||
this.getRequestContext(options)
|
||||
);
|
||||
return await this.prepareResolvedRoutes(routes, async route => {
|
||||
const prepared = await getProviderRuntimeHost(
|
||||
route.provider
|
||||
).prepare.chat(
|
||||
kind,
|
||||
{ ...cond, modelId: route.modelId },
|
||||
messages,
|
||||
options,
|
||||
route.execution
|
||||
);
|
||||
const normalizedPrepared = prepared?.route ? prepared : undefined;
|
||||
if (!normalizedPrepared) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
modelId: normalizedPrepared.route.model,
|
||||
prepared: normalizedPrepared,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async prepareStructuredRoutes(
|
||||
cond: ModelFullConditions,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotStructuredOptions = {},
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {},
|
||||
responseContract?: RequiredStructuredOutputContract
|
||||
): Promise<ResolvedCopilotProvider[]> {
|
||||
const routes = await this.resolveRoutes(
|
||||
cond,
|
||||
filter,
|
||||
this.getRequestContext(options)
|
||||
);
|
||||
return await this.prepareResolvedRoutes(routes, async route => {
|
||||
const preparedStructured =
|
||||
(await getProviderRuntimeHost(route.provider).prepare.structured(
|
||||
{ ...cond, modelId: route.modelId },
|
||||
messages,
|
||||
options,
|
||||
responseContract,
|
||||
route.execution
|
||||
)) ?? undefined;
|
||||
if (!preparedStructured) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
modelId: preparedStructured.route.model,
|
||||
preparedStructured,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async prepareEmbeddingRoutes(
|
||||
modelId: string,
|
||||
input: string | string[],
|
||||
options: CopilotEmbeddingOptions = {}
|
||||
): Promise<ResolvedCopilotProvider[]> {
|
||||
const routes = await this.resolveRoutes(
|
||||
{ modelId, outputType: ModelOutputType.Embedding },
|
||||
{},
|
||||
{
|
||||
...this.getRequestContext(options),
|
||||
featureKind: options?.featureKind ?? 'embedding',
|
||||
}
|
||||
);
|
||||
return await this.prepareResolvedRoutes(routes, async route => {
|
||||
const preparedEmbedding =
|
||||
(await getProviderRuntimeHost(route.provider).prepare.embedding(
|
||||
{ modelId: route.modelId },
|
||||
input,
|
||||
options,
|
||||
route.execution
|
||||
)) ?? undefined;
|
||||
if (!preparedEmbedding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
modelId: preparedEmbedding.route.model,
|
||||
preparedEmbedding,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async prepareRerankRoutes(
|
||||
modelId: string,
|
||||
request: CopilotRerankRequest,
|
||||
options: CopilotChatOptions = {}
|
||||
): Promise<ResolvedCopilotProvider[]> {
|
||||
const routes = await this.resolveRoutes(
|
||||
{
|
||||
modelId,
|
||||
outputType: ModelOutputType.Rerank,
|
||||
},
|
||||
{},
|
||||
{ ...this.getRequestContext(options), featureKind: 'rerank' }
|
||||
);
|
||||
return await this.prepareResolvedRoutes(routes, async route => {
|
||||
const preparedRerank =
|
||||
(await getProviderRuntimeHost(route.provider).prepare.rerank(
|
||||
{ modelId: route.modelId },
|
||||
request,
|
||||
options,
|
||||
route.execution
|
||||
)) ?? undefined;
|
||||
if (!preparedRerank) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
modelId: preparedRerank.route.model,
|
||||
preparedRerank,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async prepareImageRoutes(
|
||||
cond: ModelFullConditions,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotImageOptions = {},
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {}
|
||||
): Promise<ResolvedCopilotProvider[]> {
|
||||
const routes = await this.resolveRoutes(cond, filter, {
|
||||
...this.getRequestContext(options),
|
||||
featureKind: options?.featureKind ?? 'image',
|
||||
});
|
||||
return await this.prepareResolvedRoutes(routes, async route => {
|
||||
const preparedImage =
|
||||
(await getProviderRuntimeHost(route.provider).prepare.image(
|
||||
{ ...cond, modelId: route.modelId },
|
||||
messages,
|
||||
options,
|
||||
route.execution
|
||||
)) ?? undefined;
|
||||
if (!preparedImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
modelId: preparedImage.route.model,
|
||||
preparedImage,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getProvider(
|
||||
cond: ModelFullConditions,
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {}
|
||||
): Promise<CopilotProvider | null> {
|
||||
return (await this.resolveProvider(cond, filter))?.provider ?? null;
|
||||
}
|
||||
|
||||
async getProviderByModel(
|
||||
modelId: string,
|
||||
filter: {
|
||||
prefer?: CopilotProviderType;
|
||||
} = {}
|
||||
): Promise<CopilotProvider | null> {
|
||||
this.logger.debug(`Resolving copilot provider for model: ${modelId}`);
|
||||
return this.getProvider({ modelId }, filter);
|
||||
}
|
||||
|
||||
register(providerId: string, provider: CopilotProvider) {
|
||||
const existed = this.#providers.get(providerId);
|
||||
if (existed?.type && existed.type !== provider.type) {
|
||||
const ids = this.#providerIdsByType.get(existed.type);
|
||||
ids?.delete(providerId);
|
||||
if (!ids?.size) {
|
||||
this.#providerIdsByType.delete(existed.type);
|
||||
}
|
||||
}
|
||||
|
||||
this.#providers.set(providerId, provider);
|
||||
|
||||
const ids = this.#providerIdsByType.get(provider.type) ?? new Set<string>();
|
||||
ids.add(providerId);
|
||||
this.#providerIdsByType.set(provider.type, ids);
|
||||
|
||||
this.logger.log(
|
||||
`Copilot provider [${provider.type}] registered as [${providerId}].`
|
||||
);
|
||||
this.server.enableFeature(ServerFeature.Copilot);
|
||||
}
|
||||
|
||||
unregister(providerId: string, provider: CopilotProvider) {
|
||||
const existed = this.#providers.get(providerId);
|
||||
if (!existed || existed !== provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#providers.delete(providerId);
|
||||
|
||||
const ids = this.#providerIdsByType.get(provider.type);
|
||||
ids?.delete(providerId);
|
||||
if (!ids?.size) {
|
||||
this.#providerIdsByType.delete(provider.type);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Copilot provider [${provider.type}] unregistered from [${providerId}].`
|
||||
);
|
||||
if (this.#providers.size === 0) {
|
||||
this.server.disableFeature(ServerFeature.Copilot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CopilotProviderSideError, UserFriendlyError } from '../../../base';
|
||||
import { CopilotProvider } from './provider';
|
||||
import type {
|
||||
CopilotProviderExecution,
|
||||
ProviderDriverSpec,
|
||||
} from './provider-runtime-contract';
|
||||
import { CopilotProviderType } from './types';
|
||||
|
||||
export type FalConfig = {
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FalProvider extends CopilotProvider<FalConfig> {
|
||||
override type = CopilotProviderType.FAL;
|
||||
|
||||
protected resolveModelBackendKind() {
|
||||
return 'fal' as const;
|
||||
}
|
||||
|
||||
override configured(execution?: CopilotProviderExecution): boolean {
|
||||
return !!this.getConfig(execution).apiKey;
|
||||
}
|
||||
|
||||
private createNativeConfig(execution?: CopilotProviderExecution) {
|
||||
return {
|
||||
base_url: 'https://fal.run',
|
||||
auth_token: this.getConfig(execution).apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
override getDriverSpec(): ProviderDriverSpec {
|
||||
return {
|
||||
createBackendConfig: execution => this.createNativeConfig(execution),
|
||||
mapError: error => this.handleError(error),
|
||||
chat: false,
|
||||
structured: false,
|
||||
embedding: false,
|
||||
rerank: false,
|
||||
image: {},
|
||||
};
|
||||
}
|
||||
|
||||
private handleError(e: any) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
// pass through user friendly errors
|
||||
return e;
|
||||
} else {
|
||||
const error = new CopilotProviderSideError({
|
||||
provider: this.type,
|
||||
kind: 'unexpected_response',
|
||||
message: e?.message || 'Unexpected fal response',
|
||||
});
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import {
|
||||
CopilotProviderSideError,
|
||||
OneMB,
|
||||
UserFriendlyError,
|
||||
} from '../../../../base';
|
||||
import {
|
||||
isInvalidStructuredOutputError,
|
||||
type LlmBackendConfig,
|
||||
llmResolveRequestIntentOptions,
|
||||
} from '../../../../native';
|
||||
import {
|
||||
admittedAttachmentToPromptAttachment,
|
||||
AttachmentAdmissionHost,
|
||||
} from '../../runtime/hosts/attachment-admission';
|
||||
import {
|
||||
planAdmittedAttachmentMaterialization,
|
||||
planHostUrlAttachmentMaterialization,
|
||||
} from '../../runtime/hosts/attachment-materialization-planner';
|
||||
import { AttachmentMaterializer } from '../../runtime/hosts/attachment-materializer';
|
||||
import { CopilotProvider } from '../provider';
|
||||
import { hasProviderModelBehaviorFlag } from '../provider-model-runtime';
|
||||
import {
|
||||
type CopilotProviderExecution,
|
||||
type ProviderDriverSpec,
|
||||
} from '../provider-runtime-contract';
|
||||
import type { PromptAttachment, PromptMessage } from '../types';
|
||||
import { promptAttachmentMimeType, promptAttachmentToUrl } from '../utils';
|
||||
|
||||
export const DEFAULT_DIMENSIONS = 256;
|
||||
const GEMINI_REMOTE_ATTACHMENT_MAX_BYTES = 64 * OneMB;
|
||||
const TRUSTED_ATTACHMENT_HOST_SUFFIXES = ['cdn.affine.pro'];
|
||||
const GEMINI_RETRY_INITIAL_DELAY_MS = 2_000;
|
||||
|
||||
function normalizeMimeType(mediaType?: string) {
|
||||
return mediaType?.split(';', 1)[0]?.trim() || 'application/octet-stream';
|
||||
}
|
||||
|
||||
export abstract class GeminiProvider<T> extends CopilotProvider<T> {
|
||||
@Inject() protected readonly attachmentMaterializer!: AttachmentMaterializer;
|
||||
@Inject()
|
||||
protected readonly attachmentAdmissionHost?: AttachmentAdmissionHost;
|
||||
|
||||
protected resolveModelBackendKind() {
|
||||
return this.type === 'geminiVertex'
|
||||
? ('gemini_vertex' as const)
|
||||
: ('gemini_api' as const);
|
||||
}
|
||||
|
||||
protected abstract createNativeConfig(
|
||||
execution?: CopilotProviderExecution
|
||||
): Promise<LlmBackendConfig>;
|
||||
|
||||
private handleError(e: any) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
return e;
|
||||
} else {
|
||||
return new CopilotProviderSideError({
|
||||
provider: this.type,
|
||||
kind: 'unexpected_response',
|
||||
message: e?.message || 'Unexpected google response',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getAttachmentAdmissionHost() {
|
||||
return (
|
||||
this.attachmentAdmissionHost ??
|
||||
new AttachmentAdmissionHost(this.attachmentMaterializer)
|
||||
);
|
||||
}
|
||||
|
||||
protected async prepareMessages(
|
||||
messages: PromptMessage[],
|
||||
backendConfig: LlmBackendConfig,
|
||||
options?: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
workspace?: string;
|
||||
session?: string;
|
||||
}
|
||||
): Promise<PromptMessage[]> {
|
||||
const prepared: PromptMessage[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
options?.signal?.throwIfAborted();
|
||||
if (!Array.isArray(message.attachments) || !message.attachments.length) {
|
||||
prepared.push(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
const attachments: PromptAttachment[] = [];
|
||||
let changed = false;
|
||||
for (const attachment of message.attachments) {
|
||||
options?.signal?.throwIfAborted();
|
||||
const rawUrl = promptAttachmentToUrl(attachment);
|
||||
if (!rawUrl || rawUrl.startsWith('data:')) {
|
||||
attachments.push(attachment);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(rawUrl);
|
||||
} catch {
|
||||
attachments.push(attachment);
|
||||
continue;
|
||||
}
|
||||
|
||||
const declaredMimeType = promptAttachmentMimeType(
|
||||
attachment,
|
||||
typeof message.params?.mimetype === 'string'
|
||||
? message.params.mimetype
|
||||
: undefined
|
||||
);
|
||||
const referencePlan = await planHostUrlAttachmentMaterialization(
|
||||
'gemini',
|
||||
backendConfig,
|
||||
{
|
||||
attachmentId: rawUrl,
|
||||
url: rawUrl,
|
||||
expectedMime: declaredMimeType
|
||||
? normalizeMimeType(declaredMimeType)
|
||||
: undefined,
|
||||
maxSize: GEMINI_REMOTE_ATTACHMENT_MAX_BYTES,
|
||||
}
|
||||
);
|
||||
if (referencePlan.mode === 'remote_reference') {
|
||||
attachments.push(attachment);
|
||||
continue;
|
||||
}
|
||||
|
||||
const admitted =
|
||||
await this.getAttachmentAdmissionHost().admitPromptAttachment(
|
||||
attachment,
|
||||
{
|
||||
userId: options?.user ?? 'provider-runtime',
|
||||
workspaceId: options?.workspace ?? 'provider-runtime',
|
||||
sessionId: options?.session,
|
||||
signal: options?.signal,
|
||||
maxBytes: referencePlan.request.maxSize,
|
||||
trustedHostSuffixes: TRUSTED_ATTACHMENT_HOST_SUFFIXES,
|
||||
}
|
||||
);
|
||||
const materialization = planAdmittedAttachmentMaterialization(admitted);
|
||||
attachments.push(
|
||||
materialization.mode === 'inline'
|
||||
? materialization.attachment
|
||||
: admittedAttachmentToPromptAttachment(admitted)
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
prepared.push(changed ? { ...message, attachments } : message);
|
||||
}
|
||||
|
||||
return prepared;
|
||||
}
|
||||
|
||||
protected async waitForStructuredRetry(
|
||||
delayMs: number,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
await delay(delayMs, undefined, signal ? { signal } : undefined);
|
||||
}
|
||||
|
||||
override getDriverSpec(): ProviderDriverSpec {
|
||||
return {
|
||||
createBackendConfig: execution => this.createNativeConfig(execution),
|
||||
mapError: error => this.handleError(error),
|
||||
chat: {
|
||||
prepareMessages: async context =>
|
||||
await this.prepareMessages(
|
||||
context.input.messages,
|
||||
context.backendConfig,
|
||||
context.options
|
||||
),
|
||||
resolveRequestOptions: async context => {
|
||||
const requestIntent = await llmResolveRequestIntentOptions({
|
||||
protocol: context.protocol,
|
||||
backendConfig: context.backendConfig,
|
||||
reasoning: {
|
||||
enabled: context.options.reasoning,
|
||||
supported:
|
||||
hasProviderModelBehaviorFlag(
|
||||
context.model,
|
||||
'reasoning_medium'
|
||||
) ||
|
||||
hasProviderModelBehaviorFlag(context.model, 'reasoning_high'),
|
||||
effort: hasProviderModelBehaviorFlag(
|
||||
context.model,
|
||||
'reasoning_high'
|
||||
)
|
||||
? 'high'
|
||||
: 'medium',
|
||||
includeReasoning:
|
||||
hasProviderModelBehaviorFlag(
|
||||
context.model,
|
||||
'reasoning_medium'
|
||||
) ||
|
||||
hasProviderModelBehaviorFlag(context.model, 'reasoning_high'),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
attachmentCapability: this.getAttachCapability(
|
||||
context.model,
|
||||
context.outputType
|
||||
),
|
||||
include: requestIntent.include,
|
||||
reasoning: requestIntent.reasoning,
|
||||
};
|
||||
},
|
||||
},
|
||||
structured: {
|
||||
prepareMessages: (inputMessages, backendConfig, structuredOptions) =>
|
||||
this.prepareMessages(inputMessages, backendConfig, structuredOptions),
|
||||
shouldRetry: async ({ error, attempt, options: structuredOptions }) => {
|
||||
const isParsingError =
|
||||
isInvalidStructuredOutputError(error) || error instanceof ZodError;
|
||||
const retryableError =
|
||||
isParsingError || !(error instanceof UserFriendlyError);
|
||||
const maxRetries = Math.max(structuredOptions.maxRetries ?? 3, 0);
|
||||
if (!retryableError || attempt >= maxRetries) {
|
||||
return false;
|
||||
}
|
||||
if (!isParsingError) {
|
||||
await this.waitForStructuredRetry(
|
||||
GEMINI_RETRY_INITIAL_DELAY_MS * 2 ** attempt,
|
||||
structuredOptions.signal
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
embedding: {
|
||||
defaultDimensions: DEFAULT_DIMENSIONS,
|
||||
taskType: 'RETRIEVAL_DOCUMENT',
|
||||
},
|
||||
rerank: false,
|
||||
image: {
|
||||
prepareMessages: (inputMessages, backendConfig, imageOptions) =>
|
||||
this.prepareMessages(inputMessages, backendConfig, imageOptions),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { LlmBackendConfig } from '../../../../native';
|
||||
import type { CopilotProviderExecution } from '../provider-runtime-contract';
|
||||
import { CopilotProviderType } from '../types';
|
||||
import { GeminiProvider } from './gemini';
|
||||
|
||||
export type GeminiGenerativeConfig = {
|
||||
apiKey: string;
|
||||
baseURL?: string;
|
||||
};
|
||||
|
||||
export class GeminiGenerativeProvider extends GeminiProvider<GeminiGenerativeConfig> {
|
||||
override readonly type = CopilotProviderType.Gemini;
|
||||
override configured(execution?: CopilotProviderExecution): boolean {
|
||||
return !!this.getConfig(execution).apiKey;
|
||||
}
|
||||
|
||||
protected override async createNativeConfig(
|
||||
execution?: CopilotProviderExecution
|
||||
): Promise<LlmBackendConfig> {
|
||||
const config = this.getConfig(execution);
|
||||
return {
|
||||
base_url: (
|
||||
config.baseURL || 'https://generativelanguage.googleapis.com/v1beta'
|
||||
).replace(/\/$/, ''),
|
||||
auth_token: config.apiKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './generative';
|
||||
export * from './vertex';
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { LlmBackendConfig } from '../../../../native';
|
||||
import type { CopilotProviderExecution } from '../provider-runtime-contract';
|
||||
import { CopilotProviderType } from '../types';
|
||||
import {
|
||||
getGoogleAuth,
|
||||
getVertexGoogleBaseUrl,
|
||||
type VertexProviderConfig,
|
||||
} from '../utils';
|
||||
import { GeminiProvider } from './gemini';
|
||||
|
||||
export type GeminiVertexConfig = VertexProviderConfig;
|
||||
|
||||
export class GeminiVertexProvider extends GeminiProvider<GeminiVertexConfig> {
|
||||
override readonly type = CopilotProviderType.GeminiVertex;
|
||||
override configured(execution?: CopilotProviderExecution): boolean {
|
||||
const config = this.getConfig(execution);
|
||||
return !!getVertexGoogleBaseUrl(config) && !!config.googleAuthOptions;
|
||||
}
|
||||
protected async resolveVertexAuth(execution?: CopilotProviderExecution) {
|
||||
return await getGoogleAuth(this.getConfig(execution), 'google');
|
||||
}
|
||||
|
||||
protected override async createNativeConfig(
|
||||
execution?: CopilotProviderExecution
|
||||
): Promise<LlmBackendConfig> {
|
||||
const auth = await this.resolveVertexAuth(execution);
|
||||
const { Authorization: authHeader } = auth.headers();
|
||||
|
||||
return {
|
||||
base_url: auth.baseUrl || '',
|
||||
auth_token: authHeader.replace(/^Bearer\s+/i, ''),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export {
|
||||
AnthropicOfficialProvider,
|
||||
AnthropicVertexProvider,
|
||||
} from './anthropic';
|
||||
export { CloudflareWorkersAIProvider } from './cloudflare';
|
||||
export { CopilotProviderFactory } from './factory';
|
||||
export { FalProvider } from './fal';
|
||||
export { GeminiGenerativeProvider, GeminiVertexProvider } from './gemini';
|
||||
export { CopilotProviderLifecycleService } from './lifecycle-service';
|
||||
export { OpenAIProvider } from './openai';
|
||||
export type { CopilotProvider } from './provider';
|
||||
export { CopilotProviders } from './provider-tokens';
|
||||
export { CopilotProviderRegistryService } from './registry-service';
|
||||
export * from './types';
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Injectable, Type } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import { OnEvent } from '../../../base';
|
||||
import { CopilotProviderFactory } from './factory';
|
||||
import type { CopilotProvider } from './provider';
|
||||
import type { CopilotProviderExecution } from './provider-runtime-contract';
|
||||
import { CopilotProviders } from './provider-tokens';
|
||||
import { CopilotProviderRegistryService } from './registry-service';
|
||||
|
||||
@Injectable()
|
||||
export class CopilotProviderLifecycleService {
|
||||
private readonly registeredByProvider = new WeakMap<
|
||||
CopilotProvider,
|
||||
Set<string>
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private readonly moduleRef: ModuleRef,
|
||||
private readonly factory: CopilotProviderFactory,
|
||||
private readonly registries: CopilotProviderRegistryService
|
||||
) {}
|
||||
|
||||
private getProviders(): CopilotProvider[] {
|
||||
return CopilotProviders.flatMap(token => {
|
||||
const provider = this.moduleRef.get(token as Type<CopilotProvider>, {
|
||||
strict: false,
|
||||
});
|
||||
return provider ? [provider] : [];
|
||||
});
|
||||
}
|
||||
|
||||
private getRegisteredProviderIds(provider: CopilotProvider) {
|
||||
const current = this.registeredByProvider.get(provider);
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const next = new Set<string>();
|
||||
this.registeredByProvider.set(provider, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
private async syncProvider(provider: CopilotProvider) {
|
||||
const registry = this.registries.getRegistry();
|
||||
const configuredIds = new Set<string>();
|
||||
|
||||
for (const providerId of registry.byType.get(provider.type) ?? []) {
|
||||
const profile = registry.profiles.get(providerId);
|
||||
if (!profile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const execution: CopilotProviderExecution = { providerId, profile };
|
||||
if (!provider.configured(execution)) {
|
||||
this.factory.unregister(providerId, provider);
|
||||
continue;
|
||||
}
|
||||
|
||||
configuredIds.add(providerId);
|
||||
this.factory.register(providerId, provider);
|
||||
}
|
||||
|
||||
const previous = this.getRegisteredProviderIds(provider);
|
||||
for (const providerId of previous) {
|
||||
if (!configuredIds.has(providerId)) {
|
||||
this.factory.unregister(providerId, provider);
|
||||
}
|
||||
}
|
||||
this.registeredByProvider.set(provider, configuredIds);
|
||||
}
|
||||
|
||||
async syncProviders() {
|
||||
for (const provider of this.getProviders()) {
|
||||
await this.syncProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent('config.init')
|
||||
async onConfigInit() {
|
||||
await this.syncProviders();
|
||||
}
|
||||
|
||||
@OnEvent('config.changed')
|
||||
async onConfigChanged(event: Events['config.changed']) {
|
||||
if ('copilot' in event.updates) {
|
||||
await this.syncProviders();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CopilotProviderSideError,
|
||||
OneMB,
|
||||
UserFriendlyError,
|
||||
} from '../../../base';
|
||||
import {
|
||||
type LlmBackendConfig,
|
||||
llmResolveRequestIntentOptions,
|
||||
} from '../../../native';
|
||||
import {
|
||||
admittedAttachmentToPromptAttachment,
|
||||
AttachmentAdmissionHost,
|
||||
} from '../runtime/hosts/attachment-admission';
|
||||
import { AttachmentMaterializer } from '../runtime/hosts/attachment-materializer';
|
||||
import { CopilotProvider } from './provider';
|
||||
import { hasProviderModelBehaviorFlag } from './provider-model-runtime';
|
||||
import type {
|
||||
CopilotProviderExecution,
|
||||
ProviderDriverSpec,
|
||||
} from './provider-runtime-contract';
|
||||
import {
|
||||
CopilotProviderType,
|
||||
type PromptAttachment,
|
||||
type PromptMessage,
|
||||
} from './types';
|
||||
import { promptAttachmentToUrl } from './utils';
|
||||
|
||||
export const DEFAULT_DIMENSIONS = 256;
|
||||
|
||||
export type OpenAIConfig = {
|
||||
apiKey: string;
|
||||
baseURL?: string;
|
||||
oldApiStyle?: boolean;
|
||||
};
|
||||
|
||||
export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
|
||||
readonly type = CopilotProviderType.OpenAI;
|
||||
@Inject() protected readonly attachmentMaterializer!: AttachmentMaterializer;
|
||||
@Inject()
|
||||
protected readonly attachmentAdmissionHost?: AttachmentAdmissionHost;
|
||||
|
||||
protected resolveModelBackendKind(execution?: CopilotProviderExecution) {
|
||||
return this.getConfig(execution).oldApiStyle
|
||||
? ('openai_chat' as const)
|
||||
: ('openai_responses' as const);
|
||||
}
|
||||
|
||||
override configured(execution?: CopilotProviderExecution): boolean {
|
||||
return !!this.getConfig(execution).apiKey;
|
||||
}
|
||||
|
||||
private handleError(e: any) {
|
||||
if (e instanceof UserFriendlyError) {
|
||||
return e;
|
||||
}
|
||||
return new CopilotProviderSideError({
|
||||
provider: this.type,
|
||||
kind: 'unexpected_response',
|
||||
message: e?.message || 'Unexpected openai response',
|
||||
});
|
||||
}
|
||||
|
||||
protected createNativeConfig(
|
||||
execution?: CopilotProviderExecution
|
||||
): LlmBackendConfig {
|
||||
const config = this.getConfig(execution);
|
||||
const baseUrl = config.baseURL || 'https://api.openai.com/v1';
|
||||
return {
|
||||
base_url: baseUrl.replace(/\/v1\/?$/, ''),
|
||||
auth_token: config.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
private getAttachmentAdmissionHost() {
|
||||
return (
|
||||
this.attachmentAdmissionHost ??
|
||||
new AttachmentAdmissionHost(this.attachmentMaterializer)
|
||||
);
|
||||
}
|
||||
|
||||
private async prepareImageMessages(
|
||||
messages: PromptMessage[],
|
||||
options: {
|
||||
signal?: AbortSignal;
|
||||
user?: string;
|
||||
workspace?: string;
|
||||
session?: string;
|
||||
}
|
||||
) {
|
||||
const prepared: PromptMessage[] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
options.signal?.throwIfAborted();
|
||||
if (!Array.isArray(message.attachments) || !message.attachments.length) {
|
||||
prepared.push(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const attachments: PromptAttachment[] = [];
|
||||
for (const attachment of message.attachments) {
|
||||
options.signal?.throwIfAborted();
|
||||
const url = promptAttachmentToUrl(attachment);
|
||||
if (!url || url.startsWith('data:')) {
|
||||
attachments.push(attachment);
|
||||
continue;
|
||||
}
|
||||
|
||||
const admitted =
|
||||
await this.getAttachmentAdmissionHost().admitPromptAttachment(
|
||||
attachment,
|
||||
{
|
||||
userId: options.user ?? 'provider-runtime',
|
||||
workspaceId: options.workspace ?? 'provider-runtime',
|
||||
sessionId: options.session,
|
||||
signal: options.signal,
|
||||
maxBytes: 50 * OneMB,
|
||||
}
|
||||
);
|
||||
attachments.push(admittedAttachmentToPromptAttachment(admitted));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
prepared.push(changed ? { ...message, attachments } : message);
|
||||
}
|
||||
|
||||
return prepared;
|
||||
}
|
||||
|
||||
override getDriverSpec(): ProviderDriverSpec {
|
||||
return {
|
||||
createBackendConfig: execution => this.createNativeConfig(execution),
|
||||
mapError: error => this.handleError(error),
|
||||
chat: {
|
||||
resolveRequestOptions: async context => {
|
||||
const requestIntent = await llmResolveRequestIntentOptions({
|
||||
protocol: context.protocol,
|
||||
backendConfig: context.backendConfig,
|
||||
include: context.options.webSearch ? ['citations'] : undefined,
|
||||
reasoning: {
|
||||
enabled: context.options.reasoning,
|
||||
supported: hasProviderModelBehaviorFlag(
|
||||
context.model,
|
||||
'reasoning_supported'
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
attachmentCapability: this.getAttachCapability(
|
||||
context.model,
|
||||
context.outputType
|
||||
),
|
||||
include: requestIntent.include,
|
||||
reasoning: requestIntent.reasoning,
|
||||
};
|
||||
},
|
||||
},
|
||||
structured: {},
|
||||
embedding: {
|
||||
defaultDimensions: DEFAULT_DIMENSIONS,
|
||||
taskType: 'RETRIEVAL_DOCUMENT',
|
||||
},
|
||||
image: {
|
||||
prepareMessages: async (messages, _backendConfig, options) =>
|
||||
await this.prepareImageMessages(messages, options),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { ProviderMiddlewareConfig } from '../config';
|
||||
import { CopilotProviderType } from './types';
|
||||
|
||||
const DEFAULT_NODE_TEXT_MIDDLEWARE: NonNullable<
|
||||
NonNullable<ProviderMiddlewareConfig['node']>['text']
|
||||
> = ['citation_footnote', 'callout'];
|
||||
|
||||
const DEFAULT_MIDDLEWARE_BY_TYPE: Record<
|
||||
CopilotProviderType,
|
||||
ProviderMiddlewareConfig
|
||||
> = {
|
||||
[CopilotProviderType.OpenAI]: {
|
||||
node: { text: DEFAULT_NODE_TEXT_MIDDLEWARE },
|
||||
},
|
||||
[CopilotProviderType.CloudflareWorkersAi]: {
|
||||
node: { text: DEFAULT_NODE_TEXT_MIDDLEWARE },
|
||||
},
|
||||
[CopilotProviderType.Anthropic]: {
|
||||
node: { text: DEFAULT_NODE_TEXT_MIDDLEWARE },
|
||||
},
|
||||
[CopilotProviderType.AnthropicVertex]: {
|
||||
node: { text: DEFAULT_NODE_TEXT_MIDDLEWARE },
|
||||
},
|
||||
[CopilotProviderType.Gemini]: {
|
||||
node: { text: DEFAULT_NODE_TEXT_MIDDLEWARE },
|
||||
},
|
||||
[CopilotProviderType.GeminiVertex]: {
|
||||
node: { text: DEFAULT_NODE_TEXT_MIDDLEWARE },
|
||||
},
|
||||
[CopilotProviderType.FAL]: {},
|
||||
};
|
||||
|
||||
function unique<T>(items: T[]) {
|
||||
return [...new Set(items)];
|
||||
}
|
||||
|
||||
function mergeArray<T>(base: T[] | undefined, override: T[] | undefined) {
|
||||
if (!base?.length && !override?.length) {
|
||||
return undefined;
|
||||
}
|
||||
return unique([...(base ?? []), ...(override ?? [])]);
|
||||
}
|
||||
|
||||
function compactMiddlewareSection<T extends Record<string, unknown>>(
|
||||
section: T
|
||||
): T | undefined {
|
||||
return Object.values(section).some(value => value !== undefined)
|
||||
? section
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function mergeProviderMiddleware(
|
||||
defaults: ProviderMiddlewareConfig,
|
||||
override?: ProviderMiddlewareConfig
|
||||
): ProviderMiddlewareConfig {
|
||||
return {
|
||||
rust: compactMiddlewareSection({
|
||||
request: mergeArray(defaults.rust?.request, override?.rust?.request),
|
||||
stream: mergeArray(defaults.rust?.stream, override?.rust?.stream),
|
||||
}),
|
||||
node: compactMiddlewareSection({
|
||||
text: mergeArray(defaults.node?.text, override?.node?.text),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveProviderMiddleware(
|
||||
type: CopilotProviderType,
|
||||
override?: ProviderMiddlewareConfig
|
||||
): ProviderMiddlewareConfig {
|
||||
const defaults = DEFAULT_MIDDLEWARE_BY_TYPE[type] ?? {};
|
||||
return mergeProviderMiddleware(defaults, override);
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CopilotPromptInvalid } from '../../../base';
|
||||
import {
|
||||
type LlmBackendConfig,
|
||||
llmInferPromptModelConditions,
|
||||
llmMatchModelCapabilities,
|
||||
llmMatchModelRegistry,
|
||||
type LlmProtocol,
|
||||
llmResolveModelRegistryVariant,
|
||||
} from '../../../native';
|
||||
import { applyPromptAttachmentMimeTypeHintForNative } from './attachments';
|
||||
import {
|
||||
type CopilotChatOptions,
|
||||
type CopilotImageOptions,
|
||||
type CopilotModelBackendKind,
|
||||
type CopilotProviderModel,
|
||||
type CopilotProviderType,
|
||||
type CopilotStructuredOptions,
|
||||
EmbeddingMessage,
|
||||
type ModelAttachmentCapability,
|
||||
type ModelCapability,
|
||||
type ModelFullConditions,
|
||||
ModelInputType,
|
||||
ModelOutputType,
|
||||
type PromptAttachmentKind,
|
||||
type PromptAttachmentSourceKind,
|
||||
type PromptMessage,
|
||||
PromptMessageSchema,
|
||||
} from './types';
|
||||
|
||||
// Owner: backend host model-selection glue.
|
||||
// Capability matching and catalog lookup are delegated to native/adapter; this
|
||||
// file keeps provider prefix/default/prefer behavior and Node prompt checks.
|
||||
export type ProviderModelRuntimeContext = {
|
||||
type: CopilotProviderType;
|
||||
backendKind: CopilotModelBackendKind;
|
||||
};
|
||||
|
||||
export type ResolvedProviderModel = CopilotProviderModel & {
|
||||
backendKind: CopilotModelBackendKind;
|
||||
canonicalKey: string;
|
||||
protocol?: LlmProtocol;
|
||||
requestLayer?: LlmBackendConfig['request_layer'];
|
||||
routeOverrides?: Partial<
|
||||
Record<
|
||||
ModelOutputType,
|
||||
{
|
||||
protocol?: LlmProtocol;
|
||||
requestLayer?: LlmBackendConfig['request_layer'];
|
||||
}
|
||||
>
|
||||
>;
|
||||
behaviorFlags?: string[];
|
||||
};
|
||||
|
||||
function unique<T>(values: Iterable<T>) {
|
||||
return Array.from(new Set(values));
|
||||
}
|
||||
|
||||
function resolveAttachmentCapability(
|
||||
cap: ModelCapability,
|
||||
outputType?: ModelOutputType
|
||||
): ModelAttachmentCapability | undefined {
|
||||
if (outputType === ModelOutputType.Structured) {
|
||||
return cap.structuredAttachments ?? cap.attachments;
|
||||
}
|
||||
return cap.attachments;
|
||||
}
|
||||
|
||||
function toProviderModel(
|
||||
variant: NonNullable<
|
||||
ReturnType<typeof llmResolveModelRegistryVariant>['variant']
|
||||
>
|
||||
): ResolvedProviderModel {
|
||||
return {
|
||||
id: variant.rawModelId,
|
||||
name: variant.displayName,
|
||||
backendKind: variant.backendKind,
|
||||
canonicalKey: variant.canonicalKey,
|
||||
protocol: variant.protocol,
|
||||
requestLayer: variant.requestLayer,
|
||||
routeOverrides: variant.routeOverrides,
|
||||
behaviorFlags: variant.behaviorFlags,
|
||||
capabilities: variant.capabilities.map(capability => ({
|
||||
input: capability.input as ModelInputType[],
|
||||
output: capability.output as ModelOutputType[],
|
||||
attachments: capability.attachments
|
||||
? {
|
||||
kinds: capability.attachments.kinds as PromptAttachmentKind[],
|
||||
sourceKinds: capability.attachments.sourceKinds as
|
||||
| ModelAttachmentCapability['sourceKinds']
|
||||
| undefined,
|
||||
allowRemoteUrls: capability.attachments.allowRemoteUrls,
|
||||
}
|
||||
: undefined,
|
||||
structuredAttachments: capability.structuredAttachments
|
||||
? {
|
||||
kinds: capability.structuredAttachments
|
||||
.kinds as PromptAttachmentKind[],
|
||||
sourceKinds: capability.structuredAttachments.sourceKinds as
|
||||
| ModelAttachmentCapability['sourceKinds']
|
||||
| undefined,
|
||||
allowRemoteUrls: capability.structuredAttachments.allowRemoteUrls,
|
||||
}
|
||||
: undefined,
|
||||
defaultForOutputType: capability.defaultForOutputType,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export type ProviderModelSelection = {
|
||||
kind: 'configured';
|
||||
model: ResolvedProviderModel;
|
||||
};
|
||||
|
||||
export function resolveProviderModelSelection(
|
||||
context: ProviderModelRuntimeContext,
|
||||
cond: ModelFullConditions
|
||||
): ProviderModelSelection | undefined {
|
||||
if (cond.modelId) {
|
||||
const resolved = llmResolveModelRegistryVariant({
|
||||
backendKind: context.backendKind,
|
||||
modelId: cond.modelId,
|
||||
}).variant;
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = toProviderModel(resolved);
|
||||
const matchedModelId = llmMatchModelCapabilities([model], {
|
||||
...cond,
|
||||
modelId: model.id,
|
||||
});
|
||||
if (!matchedModelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'configured',
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
const resolved = llmMatchModelRegistry({
|
||||
backendKind: context.backendKind,
|
||||
cond,
|
||||
}).variant;
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'configured',
|
||||
model: toProviderModel(resolved),
|
||||
};
|
||||
}
|
||||
|
||||
function isMultimodal(model: CopilotProviderModel) {
|
||||
return model.capabilities.some(c =>
|
||||
[ModelInputType.Image, ModelInputType.Audio, ModelInputType.File].some(t =>
|
||||
c.input.includes(t)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function handleZodError(ret: z.SafeParseReturnType<any, any>) {
|
||||
if (ret.success) return;
|
||||
const issues = ret.error.issues.map(i => {
|
||||
const path =
|
||||
'root' +
|
||||
(i.path.length
|
||||
? `.${i.path.map(seg => (typeof seg === 'number' ? `[${seg}]` : `.${seg}`)).join('')}`
|
||||
: '');
|
||||
return `${i.message}${path}`;
|
||||
});
|
||||
throw new CopilotPromptInvalid(issues.join('; '));
|
||||
}
|
||||
|
||||
export async function inferModelConditionsFromMessages(
|
||||
messages?: PromptMessage[],
|
||||
withAttachment = true
|
||||
): Promise<Partial<ModelFullConditions>> {
|
||||
if (!messages?.length || !withAttachment) return {};
|
||||
const projectedMessages = messages.map(message => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
...(Array.isArray(message.attachments) && message.attachments.length
|
||||
? {
|
||||
attachments: message.attachments.map(attachment =>
|
||||
applyPromptAttachmentMimeTypeHintForNative(attachment, message)
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
const inferredCond = llmInferPromptModelConditions(projectedMessages);
|
||||
|
||||
return {
|
||||
...(inferredCond.attachmentKinds?.length
|
||||
? { attachmentKinds: unique(inferredCond.attachmentKinds) }
|
||||
: {}),
|
||||
...(inferredCond.attachmentSourceKinds?.length
|
||||
? {
|
||||
attachmentSourceKinds: unique(
|
||||
inferredCond.attachmentSourceKinds
|
||||
) as PromptAttachmentSourceKind[],
|
||||
}
|
||||
: {}),
|
||||
...(inferredCond.inputTypes?.length
|
||||
? { inputTypes: unique(inferredCond.inputTypes) as ModelInputType[] }
|
||||
: {}),
|
||||
...(inferredCond.hasRemoteAttachments
|
||||
? { hasRemoteAttachments: true }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeModelConditions(
|
||||
cond: ModelFullConditions,
|
||||
inferredCond: Partial<ModelFullConditions>
|
||||
): ModelFullConditions {
|
||||
return {
|
||||
...inferredCond,
|
||||
...cond,
|
||||
inputTypes: unique([
|
||||
...(inferredCond.inputTypes ?? []),
|
||||
...(cond.inputTypes ?? []),
|
||||
]),
|
||||
attachmentKinds: unique([
|
||||
...(inferredCond.attachmentKinds ?? []),
|
||||
...(cond.attachmentKinds ?? []),
|
||||
]),
|
||||
attachmentSourceKinds: unique([
|
||||
...(inferredCond.attachmentSourceKinds ?? []),
|
||||
...(cond.attachmentSourceKinds ?? []),
|
||||
]),
|
||||
hasRemoteAttachments:
|
||||
cond.hasRemoteAttachments ?? inferredCond.hasRemoteAttachments,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAttachCapability(
|
||||
model: CopilotProviderModel,
|
||||
outputType: ModelOutputType
|
||||
): ModelAttachmentCapability | undefined {
|
||||
const capability =
|
||||
model.capabilities.find(cap => cap.output.includes(outputType)) ??
|
||||
model.capabilities[0];
|
||||
if (!capability) {
|
||||
return;
|
||||
}
|
||||
return resolveAttachmentCapability(capability, outputType);
|
||||
}
|
||||
|
||||
export function matchProviderModel(
|
||||
context: ProviderModelRuntimeContext,
|
||||
cond: ModelFullConditions
|
||||
): boolean {
|
||||
return !!resolveProviderModelSelection(context, cond);
|
||||
}
|
||||
|
||||
export function resolveProviderModel(
|
||||
context: ProviderModelRuntimeContext,
|
||||
modelId: string
|
||||
): ResolvedProviderModel | undefined {
|
||||
return resolveProviderModelSelection(context, {
|
||||
modelId,
|
||||
})?.model;
|
||||
}
|
||||
|
||||
export function hasProviderModelBehaviorFlag(
|
||||
model: CopilotProviderModel,
|
||||
flag: string
|
||||
) {
|
||||
const behaviorFlags = (model as ResolvedProviderModel).behaviorFlags;
|
||||
return Array.isArray(behaviorFlags) && behaviorFlags.includes(flag);
|
||||
}
|
||||
|
||||
export function resolveProviderModelRoute(
|
||||
model: CopilotProviderModel,
|
||||
outputType: ModelOutputType
|
||||
) {
|
||||
const resolved = model as ResolvedProviderModel;
|
||||
const override = resolved.routeOverrides?.[outputType];
|
||||
|
||||
return {
|
||||
protocol: override?.protocol ?? resolved.protocol,
|
||||
requestLayer: override?.requestLayer ?? resolved.requestLayer,
|
||||
};
|
||||
}
|
||||
|
||||
export function requireProviderModelSelection(
|
||||
context: ProviderModelRuntimeContext,
|
||||
cond: ModelFullConditions
|
||||
): ResolvedProviderModel {
|
||||
const selection = resolveProviderModelSelection(context, cond);
|
||||
if (selection) return selection.model;
|
||||
|
||||
const { modelId, outputType, inputTypes } = cond;
|
||||
throw new CopilotPromptInvalid(
|
||||
modelId
|
||||
? `Model ${modelId} does not support ${outputType ?? '<any>'} output with ${inputTypes ?? '<any>'} input`
|
||||
: outputType
|
||||
? `No model supports ${outputType} output with ${inputTypes ?? '<any>'} input for provider ${context.type}`
|
||||
: 'Output type is required when modelId is not provided'
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkProviderParams(
|
||||
context: ProviderModelRuntimeContext,
|
||||
{
|
||||
cond,
|
||||
messages,
|
||||
embeddings,
|
||||
options = {},
|
||||
withAttachment = true,
|
||||
}: {
|
||||
cond: ModelFullConditions;
|
||||
messages?: PromptMessage[];
|
||||
embeddings?: string[];
|
||||
options?:
|
||||
| CopilotChatOptions
|
||||
| CopilotStructuredOptions
|
||||
| CopilotImageOptions;
|
||||
withAttachment?: boolean;
|
||||
execution?: unknown;
|
||||
}
|
||||
): Promise<ModelFullConditions> {
|
||||
if (messages) {
|
||||
const { requireContent = true, requireAttachment = false } = options;
|
||||
|
||||
const MessageSchema = z
|
||||
.array(
|
||||
PromptMessageSchema.extend({
|
||||
content: requireContent
|
||||
? z.string().trim().min(1)
|
||||
: z.string().optional().nullable(),
|
||||
})
|
||||
.passthrough()
|
||||
.catchall(z.union([z.string(), z.number(), z.date(), z.null()]))
|
||||
)
|
||||
.optional();
|
||||
|
||||
handleZodError(MessageSchema.safeParse(messages));
|
||||
|
||||
const inferredCond = await inferModelConditionsFromMessages(
|
||||
messages,
|
||||
withAttachment
|
||||
);
|
||||
const mergedCond = mergeModelConditions(cond, inferredCond);
|
||||
const model = requireProviderModelSelection(context, mergedCond);
|
||||
const multimodal = isMultimodal(model);
|
||||
|
||||
if (
|
||||
multimodal &&
|
||||
requireAttachment &&
|
||||
!messages.some(
|
||||
message =>
|
||||
message.role === 'user' &&
|
||||
Array.isArray(message.attachments) &&
|
||||
message.attachments.length > 0
|
||||
)
|
||||
) {
|
||||
throw new CopilotPromptInvalid('attachments required in multimodal mode');
|
||||
}
|
||||
|
||||
if (embeddings) {
|
||||
handleZodError(EmbeddingMessage.safeParse(embeddings));
|
||||
}
|
||||
|
||||
return mergedCond;
|
||||
}
|
||||
|
||||
const inferredCond = await inferModelConditionsFromMessages(
|
||||
messages,
|
||||
withAttachment
|
||||
);
|
||||
const mergedCond = mergeModelConditions(cond, inferredCond);
|
||||
|
||||
if (embeddings) {
|
||||
handleZodError(EmbeddingMessage.safeParse(embeddings));
|
||||
}
|
||||
|
||||
return mergedCond;
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
import type {
|
||||
LlmBackendConfig,
|
||||
LlmEmbeddingRequest,
|
||||
LlmProtocol,
|
||||
LlmRerankRequest,
|
||||
LlmStructuredRequest,
|
||||
} from '../../../native';
|
||||
import {
|
||||
buildLlmImageRequestFromMessages,
|
||||
llmEmbeddingDispatch,
|
||||
llmRerankDispatch,
|
||||
llmStructuredDispatch,
|
||||
} from '../../../native';
|
||||
import type { NodeTextMiddleware, ProviderMiddlewareConfig } from '../config';
|
||||
import {
|
||||
buildToolContracts,
|
||||
projectPromptMessageForNative,
|
||||
} from '../runtime/contracts';
|
||||
import { buildNativeRequest } from '../runtime/native-request-runtime';
|
||||
import type { ToolLoopBackend } from '../runtime/tool/bridge';
|
||||
import type { NativeProviderAdapter } from '../runtime/tool/native-adapter';
|
||||
import type { CopilotToolSet } from '../tools';
|
||||
import type {
|
||||
CopilotProviderExecution,
|
||||
PreparedNativeEmbeddingExecution,
|
||||
PreparedNativeExecution,
|
||||
PreparedNativeImageExecution,
|
||||
PreparedNativeRequestOptions,
|
||||
PreparedNativeRerankExecution,
|
||||
PreparedNativeStructuredExecution,
|
||||
} from './provider-runtime-contract';
|
||||
import type {
|
||||
CopilotChatOptions,
|
||||
CopilotImageOptions,
|
||||
PromptMessage,
|
||||
} from './types';
|
||||
|
||||
export type CreateToolAdapterOptions = {
|
||||
maxSteps?: number;
|
||||
nodeTextMiddleware?: NodeTextMiddleware[];
|
||||
};
|
||||
|
||||
export type CreateNativeAdapter = (
|
||||
backend: ToolLoopBackend,
|
||||
tools: CopilotToolSet,
|
||||
nodeTextMiddleware?: NodeTextMiddleware[],
|
||||
options?: CreateToolAdapterOptions
|
||||
) => NativeProviderAdapter;
|
||||
|
||||
export type CreatePreparedExecutionRuntimeInput = {
|
||||
resolveProviderId: (execution?: CopilotProviderExecution) => string;
|
||||
getTools: (
|
||||
options: CopilotChatOptions,
|
||||
model: string
|
||||
) => Promise<CopilotToolSet>;
|
||||
getActiveProviderMiddleware: (
|
||||
execution?: CopilotProviderExecution
|
||||
) => ProviderMiddlewareConfig;
|
||||
createNativeAdapter: CreateNativeAdapter;
|
||||
maxSteps: number;
|
||||
};
|
||||
export type PreparedExecutionRuntime = ReturnType<
|
||||
typeof createPreparedExecutionRuntime
|
||||
>;
|
||||
|
||||
export function createPreparedExecutionRuntime(
|
||||
input: CreatePreparedExecutionRuntimeInput
|
||||
) {
|
||||
return {
|
||||
buildPreparedNativeExecution: async (
|
||||
prepared: PreparedNativeRequestOptions
|
||||
) =>
|
||||
await buildPreparedNativeExecution(
|
||||
input.resolveProviderId(prepared.execution),
|
||||
input.getTools,
|
||||
input.getActiveProviderMiddleware,
|
||||
input.maxSteps,
|
||||
prepared
|
||||
),
|
||||
createPreparedExecutionAdapter: (prepared: PreparedNativeExecution) =>
|
||||
createPreparedExecutionAdapter(
|
||||
input.createNativeAdapter,
|
||||
input.maxSteps,
|
||||
prepared
|
||||
),
|
||||
buildPreparedNativeStructuredExecution: (
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
request: LlmStructuredRequest,
|
||||
execution?: CopilotProviderExecution
|
||||
) =>
|
||||
buildPreparedNativeStructuredExecution(
|
||||
input.resolveProviderId(execution),
|
||||
protocol,
|
||||
backendConfig,
|
||||
model,
|
||||
request
|
||||
),
|
||||
buildPreparedNativeEmbeddingExecution: (
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
request: LlmEmbeddingRequest,
|
||||
execution?: CopilotProviderExecution
|
||||
) =>
|
||||
buildPreparedNativeEmbeddingExecution(
|
||||
input.resolveProviderId(execution),
|
||||
protocol,
|
||||
backendConfig,
|
||||
model,
|
||||
request
|
||||
),
|
||||
buildPreparedNativeRerankExecution: (
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
request: LlmRerankRequest,
|
||||
execution?: CopilotProviderExecution
|
||||
) =>
|
||||
buildPreparedNativeRerankExecution(
|
||||
input.resolveProviderId(execution),
|
||||
protocol,
|
||||
backendConfig,
|
||||
model,
|
||||
request
|
||||
),
|
||||
buildPreparedNativeImageExecution: (
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotImageOptions = {},
|
||||
execution?: CopilotProviderExecution
|
||||
) =>
|
||||
buildPreparedNativeImageExecution(
|
||||
input.resolveProviderId(execution),
|
||||
protocol,
|
||||
backendConfig,
|
||||
model,
|
||||
messages,
|
||||
options
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function createPreparedExecutionAdapter(
|
||||
createNativeAdapter: CreateNativeAdapter,
|
||||
maxSteps: number,
|
||||
prepared: PreparedNativeExecution
|
||||
) {
|
||||
return createNativeAdapter(
|
||||
{
|
||||
protocol: prepared.route.protocol,
|
||||
backendConfig: prepared.route.backendConfig,
|
||||
},
|
||||
prepared.tools,
|
||||
prepared.postprocess?.nodeTextMiddleware,
|
||||
{
|
||||
maxSteps,
|
||||
nodeTextMiddleware: prepared.postprocess?.nodeTextMiddleware,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function createNativeStructuredDispatch(
|
||||
backendConfig: LlmBackendConfig,
|
||||
protocol: LlmProtocol
|
||||
) {
|
||||
return (request: LlmStructuredRequest) =>
|
||||
llmStructuredDispatch(protocol, backendConfig, request);
|
||||
}
|
||||
|
||||
export function createNativeEmbeddingDispatch(
|
||||
backendConfig: LlmBackendConfig,
|
||||
protocol: LlmProtocol
|
||||
) {
|
||||
return (request: LlmEmbeddingRequest) =>
|
||||
llmEmbeddingDispatch(protocol, backendConfig, request);
|
||||
}
|
||||
|
||||
export function createNativeRerankDispatch(
|
||||
backendConfig: LlmBackendConfig,
|
||||
protocol: LlmProtocol
|
||||
) {
|
||||
return (request: LlmRerankRequest) =>
|
||||
llmRerankDispatch(protocol, backendConfig, request);
|
||||
}
|
||||
|
||||
function buildPreparedRoute(
|
||||
providerId: string,
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string
|
||||
): PreparedNativeExecution['route'] {
|
||||
return {
|
||||
providerId,
|
||||
protocol,
|
||||
requestLayer: backendConfig.request_layer,
|
||||
model,
|
||||
backendConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildPreparedNativeExecution(
|
||||
providerId: string,
|
||||
getTools: (
|
||||
options: CopilotChatOptions,
|
||||
model: string
|
||||
) => Promise<CopilotToolSet>,
|
||||
getActiveProviderMiddleware: (
|
||||
execution?: CopilotProviderExecution
|
||||
) => ProviderMiddlewareConfig,
|
||||
maxSteps: number,
|
||||
{
|
||||
protocol,
|
||||
backendConfig,
|
||||
model,
|
||||
messages,
|
||||
options = {},
|
||||
execution,
|
||||
withAttachment = true,
|
||||
attachmentCapability,
|
||||
include,
|
||||
reasoning,
|
||||
tools,
|
||||
middleware,
|
||||
}: PreparedNativeRequestOptions
|
||||
): Promise<PreparedNativeExecution> {
|
||||
const resolvedTools = tools ?? (await getTools(options, model));
|
||||
const resolvedMiddleware =
|
||||
middleware ?? getActiveProviderMiddleware(execution);
|
||||
const { request } = await buildNativeRequest({
|
||||
model,
|
||||
messages,
|
||||
options,
|
||||
toolContracts: buildToolContracts(resolvedTools),
|
||||
withAttachment,
|
||||
attachmentCapability,
|
||||
include,
|
||||
reasoning,
|
||||
middleware: resolvedMiddleware,
|
||||
});
|
||||
|
||||
return {
|
||||
route: buildPreparedRoute(providerId, protocol, backendConfig, model),
|
||||
request,
|
||||
tools: resolvedTools,
|
||||
maxSteps,
|
||||
postprocess: {
|
||||
nodeTextMiddleware: resolvedMiddleware.node?.text,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type BuildPreparedNativeDispatchExecution = <
|
||||
TRequest extends
|
||||
| LlmStructuredRequest
|
||||
| LlmEmbeddingRequest
|
||||
| LlmRerankRequest,
|
||||
>(
|
||||
providerId: string,
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
request: TRequest
|
||||
) => {
|
||||
route: PreparedNativeExecution['route'];
|
||||
request: TRequest;
|
||||
};
|
||||
|
||||
const buildPreparedNativeDispatchExecution: BuildPreparedNativeDispatchExecution =
|
||||
(providerId, protocol, backendConfig, model, request) => {
|
||||
return {
|
||||
route: buildPreparedRoute(providerId, protocol, backendConfig, model),
|
||||
request,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildPreparedNativeStructuredExecution =
|
||||
buildPreparedNativeDispatchExecution as (
|
||||
providerId: string,
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
request: LlmStructuredRequest
|
||||
) => PreparedNativeStructuredExecution;
|
||||
|
||||
export const buildPreparedNativeEmbeddingExecution =
|
||||
buildPreparedNativeDispatchExecution as (
|
||||
providerId: string,
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
request: LlmEmbeddingRequest
|
||||
) => PreparedNativeEmbeddingExecution;
|
||||
|
||||
export const buildPreparedNativeRerankExecution =
|
||||
buildPreparedNativeDispatchExecution as (
|
||||
providerId: string,
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
request: LlmRerankRequest
|
||||
) => PreparedNativeRerankExecution;
|
||||
|
||||
export function buildPreparedNativeImageExecution(
|
||||
providerId: string,
|
||||
protocol: LlmProtocol,
|
||||
backendConfig: LlmBackendConfig,
|
||||
model: string,
|
||||
messages: PromptMessage[],
|
||||
options: CopilotImageOptions = {}
|
||||
): PreparedNativeImageExecution {
|
||||
const nativeMessages = messages.map(
|
||||
message => projectPromptMessageForNative(message).message
|
||||
);
|
||||
|
||||
return {
|
||||
route: buildPreparedRoute(providerId, protocol, backendConfig, model),
|
||||
request: buildLlmImageRequestFromMessages({
|
||||
model,
|
||||
protocol,
|
||||
messages: nativeMessages,
|
||||
options: projectImageRequestOptions(options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function projectImageRequestOptions(options: CopilotImageOptions = {}) {
|
||||
return {
|
||||
quality: options.quality,
|
||||
seed: options.seed,
|
||||
modelName: options.modelName,
|
||||
loras: options.loras,
|
||||
};
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
import type {
|
||||
CopilotProviderConfigMap,
|
||||
CopilotProviderDefaults,
|
||||
CopilotProviderProfile,
|
||||
ProviderMiddlewareConfig,
|
||||
} from '../config';
|
||||
import { resolveProviderMiddleware } from './provider-middleware';
|
||||
import { CopilotProviderType, ModelOutputType } from './types';
|
||||
|
||||
const PROVIDER_ID_PATTERN = /^[a-zA-Z0-9-_]+$/;
|
||||
|
||||
const LEGACY_PROVIDER_ORDER: CopilotProviderType[] = [
|
||||
CopilotProviderType.OpenAI,
|
||||
CopilotProviderType.CloudflareWorkersAi,
|
||||
CopilotProviderType.FAL,
|
||||
CopilotProviderType.Gemini,
|
||||
CopilotProviderType.GeminiVertex,
|
||||
CopilotProviderType.Anthropic,
|
||||
CopilotProviderType.AnthropicVertex,
|
||||
];
|
||||
|
||||
const LEGACY_PROVIDER_PRIORITY = LEGACY_PROVIDER_ORDER.reduce(
|
||||
(acc, type, index) => {
|
||||
acc[type] = LEGACY_PROVIDER_ORDER.length - index;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<CopilotProviderType, number>
|
||||
);
|
||||
|
||||
type LegacyProvidersConfig = Partial<
|
||||
Record<CopilotProviderType, CopilotProviderConfigMap[CopilotProviderType]>
|
||||
>;
|
||||
|
||||
export type CopilotProvidersConfigInput = LegacyProvidersConfig & {
|
||||
profiles?: CopilotProviderProfile[] | null;
|
||||
defaults?: CopilotProviderDefaults | null;
|
||||
};
|
||||
|
||||
export type NormalizedCopilotProviderProfile = Omit<
|
||||
CopilotProviderProfile,
|
||||
'enabled' | 'priority' | 'middleware'
|
||||
> & {
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
middleware: ProviderMiddlewareConfig;
|
||||
};
|
||||
|
||||
export type CopilotProviderRegistry = {
|
||||
profiles: Map<string, NormalizedCopilotProviderProfile>;
|
||||
defaults: CopilotProviderDefaults;
|
||||
order: string[];
|
||||
byType: Map<CopilotProviderType, string[]>;
|
||||
};
|
||||
|
||||
export type ResolveModelResult = {
|
||||
rawModelId?: string;
|
||||
modelId?: string;
|
||||
explicitProviderId?: string;
|
||||
candidateProviderIds: string[];
|
||||
};
|
||||
|
||||
type ResolveModelOptions = {
|
||||
registry: CopilotProviderRegistry;
|
||||
modelId?: string;
|
||||
outputType?: ModelOutputType;
|
||||
availableProviderIds?: Iterable<string>;
|
||||
preferredProviderIds?: Iterable<string>;
|
||||
};
|
||||
|
||||
function unique<T>(list: T[]): T[] {
|
||||
return [...new Set(list)];
|
||||
}
|
||||
|
||||
function asArray<T>(iter?: Iterable<T>): T[] {
|
||||
return iter ? Array.from(iter) : [];
|
||||
}
|
||||
|
||||
function parseModelPrefix(
|
||||
registry: CopilotProviderRegistry,
|
||||
modelId: string
|
||||
): { providerId: string; modelId?: string } | null {
|
||||
const index = modelId.indexOf('/');
|
||||
if (index <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerId = modelId.slice(0, index);
|
||||
if (!registry.profiles.has(providerId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const model = modelId.slice(index + 1);
|
||||
return { providerId, modelId: model || undefined };
|
||||
}
|
||||
|
||||
function normalizeProfile(
|
||||
profile: CopilotProviderProfile
|
||||
): NormalizedCopilotProviderProfile {
|
||||
return {
|
||||
...profile,
|
||||
enabled: profile.enabled !== false,
|
||||
priority: profile.priority ?? 0,
|
||||
middleware: resolveProviderMiddleware(profile.type, profile.middleware),
|
||||
};
|
||||
}
|
||||
|
||||
function toLegacyProfiles(
|
||||
config: CopilotProvidersConfigInput
|
||||
): CopilotProviderProfile[] {
|
||||
const legacyProfiles: CopilotProviderProfile[] = [];
|
||||
for (const type of LEGACY_PROVIDER_ORDER) {
|
||||
const legacyConfig = config[type];
|
||||
if (!legacyConfig) {
|
||||
continue;
|
||||
}
|
||||
legacyProfiles.push({
|
||||
id: `${type}-default`,
|
||||
type,
|
||||
priority: LEGACY_PROVIDER_PRIORITY[type],
|
||||
config: legacyConfig,
|
||||
} as CopilotProviderProfile);
|
||||
}
|
||||
return legacyProfiles;
|
||||
}
|
||||
|
||||
function mergeProfiles(
|
||||
explicitProfiles: CopilotProviderProfile[],
|
||||
legacyProfiles: CopilotProviderProfile[]
|
||||
): CopilotProviderProfile[] {
|
||||
const profiles = new Map<string, CopilotProviderProfile>();
|
||||
|
||||
for (const profile of explicitProfiles) {
|
||||
if (!PROVIDER_ID_PATTERN.test(profile.id)) {
|
||||
throw new Error(`Invalid copilot provider profile id: ${profile.id}`);
|
||||
}
|
||||
if (profiles.has(profile.id)) {
|
||||
throw new Error(`Duplicated copilot provider profile id: ${profile.id}`);
|
||||
}
|
||||
profiles.set(profile.id, profile);
|
||||
}
|
||||
|
||||
for (const profile of legacyProfiles) {
|
||||
if (!profiles.has(profile.id)) {
|
||||
profiles.set(profile.id, profile);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(profiles.values());
|
||||
}
|
||||
|
||||
function sortProfiles(profiles: NormalizedCopilotProviderProfile[]) {
|
||||
return profiles.toSorted((a, b) => {
|
||||
if (a.priority !== b.priority) {
|
||||
return b.priority - a.priority;
|
||||
}
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
|
||||
function assertDefaults(
|
||||
defaults: CopilotProviderDefaults,
|
||||
profiles: Map<string, NormalizedCopilotProviderProfile>
|
||||
) {
|
||||
for (const providerId of Object.values(defaults)) {
|
||||
if (!providerId) {
|
||||
continue;
|
||||
}
|
||||
if (!profiles.has(providerId)) {
|
||||
throw new Error(
|
||||
`Copilot provider defaults references unknown providerId: ${providerId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildProviderRegistry(
|
||||
config: CopilotProvidersConfigInput
|
||||
): CopilotProviderRegistry {
|
||||
const explicitProfiles = config.profiles ?? [];
|
||||
const legacyProfiles = toLegacyProfiles(config);
|
||||
const mergedProfiles = mergeProfiles(explicitProfiles, legacyProfiles)
|
||||
.map(normalizeProfile)
|
||||
.filter(profile => profile.enabled);
|
||||
const sortedProfiles = sortProfiles(mergedProfiles);
|
||||
|
||||
const profiles = new Map(
|
||||
sortedProfiles.map(profile => [profile.id, profile] as const)
|
||||
);
|
||||
const defaults = config.defaults ?? {};
|
||||
assertDefaults(defaults, profiles);
|
||||
|
||||
const order = sortedProfiles.map(profile => profile.id);
|
||||
const byType = new Map<CopilotProviderType, string[]>();
|
||||
for (const profile of sortedProfiles) {
|
||||
const ids = byType.get(profile.type) ?? [];
|
||||
ids.push(profile.id);
|
||||
byType.set(profile.type, ids);
|
||||
}
|
||||
|
||||
return { profiles, defaults, order, byType };
|
||||
}
|
||||
|
||||
export function resolveModel({
|
||||
registry,
|
||||
modelId,
|
||||
outputType,
|
||||
availableProviderIds,
|
||||
preferredProviderIds,
|
||||
}: ResolveModelOptions): ResolveModelResult {
|
||||
const available = new Set(asArray(availableProviderIds));
|
||||
const preferred = new Set(asArray(preferredProviderIds));
|
||||
const hasAvailableFilter = available.size > 0;
|
||||
const hasPreferredFilter = preferred.size > 0;
|
||||
|
||||
const isAllowed = (providerId: string) => {
|
||||
const profile = registry.profiles.get(providerId);
|
||||
if (!profile?.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (hasAvailableFilter && !available.has(providerId)) {
|
||||
return false;
|
||||
}
|
||||
if (hasPreferredFilter && !preferred.has(providerId)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const prefixed = modelId ? parseModelPrefix(registry, modelId) : null;
|
||||
if (prefixed) {
|
||||
return {
|
||||
rawModelId: modelId,
|
||||
modelId: prefixed.modelId,
|
||||
explicitProviderId: prefixed.providerId,
|
||||
candidateProviderIds: isAllowed(prefixed.providerId)
|
||||
? [prefixed.providerId]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (modelId) {
|
||||
return {
|
||||
rawModelId: modelId,
|
||||
modelId,
|
||||
candidateProviderIds: registry.order.filter(providerId =>
|
||||
isAllowed(providerId)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const defaultProviderId =
|
||||
outputType && outputType !== ModelOutputType.Rerank
|
||||
? registry.defaults[outputType]
|
||||
: undefined;
|
||||
|
||||
const fallbackOrder = [
|
||||
...(defaultProviderId ? [defaultProviderId] : []),
|
||||
registry.defaults.fallback,
|
||||
...registry.order,
|
||||
].filter((id): id is string => !!id);
|
||||
|
||||
return {
|
||||
rawModelId: modelId,
|
||||
modelId,
|
||||
candidateProviderIds: unique(
|
||||
fallbackOrder.filter(providerId => isAllowed(providerId))
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function stripProviderPrefix(
|
||||
registry: CopilotProviderRegistry,
|
||||
providerId: string,
|
||||
modelId?: string
|
||||
) {
|
||||
if (!modelId) {
|
||||
return modelId;
|
||||
}
|
||||
const prefixed = parseModelPrefix(registry, modelId);
|
||||
if (!prefixed) {
|
||||
return modelId;
|
||||
}
|
||||
if (prefixed.providerId !== providerId) {
|
||||
return modelId;
|
||||
}
|
||||
return prefixed.modelId;
|
||||
}
|
||||
@@ -1,456 +0,0 @@
|
||||
import type {
|
||||
LlmBackendConfig,
|
||||
LlmEmbeddingRequest,
|
||||
LlmImageRequest,
|
||||
LlmProtocol,
|
||||
LlmRequest,
|
||||
LlmRerankRequest,
|
||||
LlmStructuredRequest,
|
||||
} from '../../../native';
|
||||
import type { NodeTextMiddleware, ProviderMiddlewareConfig } from '../config';
|
||||
import type { CopilotToolSet } from '../tools';
|
||||
import {
|
||||
type ProviderModelRuntimeContext,
|
||||
resolveProviderModelRoute,
|
||||
} from './provider-model-runtime';
|
||||
import type { NormalizedCopilotProviderProfile } from './provider-registry';
|
||||
import {
|
||||
CopilotChatOptions,
|
||||
CopilotImageOptions,
|
||||
CopilotProviderModel,
|
||||
CopilotStructuredOptions,
|
||||
ModelAttachmentCapability,
|
||||
ModelConditions,
|
||||
ModelFullConditions,
|
||||
ModelOutputType,
|
||||
PromptMessage,
|
||||
} from './types';
|
||||
|
||||
export type NativeExecutionRoute = {
|
||||
protocol: LlmProtocol;
|
||||
requestLayer?: LlmBackendConfig['request_layer'];
|
||||
model: string;
|
||||
backendConfig: LlmBackendConfig;
|
||||
};
|
||||
|
||||
export type CopilotProviderExecution = {
|
||||
providerId: string;
|
||||
profile: NormalizedCopilotProviderProfile;
|
||||
};
|
||||
|
||||
export type PreparedNativeExecution = {
|
||||
route: NativeExecutionRoute & {
|
||||
providerId: string;
|
||||
};
|
||||
request: LlmRequest;
|
||||
tools: CopilotToolSet;
|
||||
maxSteps?: number;
|
||||
postprocess?: {
|
||||
nodeTextMiddleware?: NodeTextMiddleware[];
|
||||
};
|
||||
};
|
||||
|
||||
export type PreparedNativeStructuredExecution = {
|
||||
route: NativeExecutionRoute & {
|
||||
providerId: string;
|
||||
};
|
||||
request: LlmStructuredRequest;
|
||||
};
|
||||
|
||||
export type PreparedNativeEmbeddingExecution = {
|
||||
route: NativeExecutionRoute & {
|
||||
providerId: string;
|
||||
};
|
||||
request: LlmEmbeddingRequest;
|
||||
};
|
||||
|
||||
export type PreparedNativeRerankExecution = {
|
||||
route: NativeExecutionRoute & {
|
||||
providerId: string;
|
||||
};
|
||||
request: LlmRerankRequest;
|
||||
};
|
||||
|
||||
export type PreparedNativeImageExecution = {
|
||||
route: NativeExecutionRoute & {
|
||||
providerId: string;
|
||||
};
|
||||
request: LlmImageRequest;
|
||||
};
|
||||
|
||||
export type PreparedNativeRequestOptions = {
|
||||
protocol: LlmProtocol;
|
||||
backendConfig: LlmBackendConfig;
|
||||
model: string;
|
||||
messages: PromptMessage[];
|
||||
options?: CopilotChatOptions;
|
||||
execution?: CopilotProviderExecution;
|
||||
withAttachment?: boolean;
|
||||
attachmentCapability?: ModelAttachmentCapability;
|
||||
include?: string[];
|
||||
reasoning?: Record<string, unknown>;
|
||||
tools?: CopilotToolSet;
|
||||
middleware?: ProviderMiddlewareConfig;
|
||||
};
|
||||
|
||||
type ProviderChatDriverPrepareResult = Omit<
|
||||
PreparedNativeRequestOptions,
|
||||
'execution' | 'options'
|
||||
>;
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
type NativeBackendConfigResolver = (
|
||||
execution?: CopilotProviderExecution
|
||||
) => Awaitable<LlmBackendConfig>;
|
||||
|
||||
export type StructuredProviderDriver = {
|
||||
createBackendConfig: NativeBackendConfigResolver;
|
||||
prepareMessages?: (
|
||||
messages: PromptMessage[],
|
||||
backendConfig: LlmBackendConfig,
|
||||
options: NonNullable<CopilotStructuredOptions>
|
||||
) => Promise<PromptMessage[]>;
|
||||
shouldRetry?: (context: {
|
||||
error: unknown;
|
||||
attempt: number;
|
||||
options: NonNullable<CopilotStructuredOptions>;
|
||||
}) => Awaitable<boolean>;
|
||||
mapError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
export type EmbeddingProviderDriver = {
|
||||
createBackendConfig: NativeBackendConfigResolver;
|
||||
defaultDimensions?: number;
|
||||
taskType?: string;
|
||||
mapError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
export type RerankProviderDriver = {
|
||||
createBackendConfig: NativeBackendConfigResolver;
|
||||
mapError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
export type ImageProviderDriver = {
|
||||
createBackendConfig: NativeBackendConfigResolver;
|
||||
prepareMessages?: (
|
||||
messages: PromptMessage[],
|
||||
backendConfig: LlmBackendConfig,
|
||||
options: NonNullable<CopilotImageOptions>
|
||||
) => Promise<PromptMessage[]>;
|
||||
mapError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
export type ProviderMetricLabels = Record<
|
||||
string,
|
||||
string | number | boolean | undefined
|
||||
>;
|
||||
|
||||
export type ProviderExecutionDrivers = {
|
||||
chat?: ProviderChatDriver;
|
||||
structured?: StructuredProviderDriver;
|
||||
embedding?: EmbeddingProviderDriver;
|
||||
rerank?: RerankProviderDriver;
|
||||
image?: ImageProviderDriver;
|
||||
};
|
||||
|
||||
export type ProviderDriverSpec = NativeProviderDriverBase & {
|
||||
chat?: NativeChatDriverOverrides | false;
|
||||
structured?: NativeStructuredDriverOverrides | false;
|
||||
embedding?: NativeEmbeddingDriverOverrides | false;
|
||||
rerank?: NativeRerankDriverOverrides | false;
|
||||
image?: NativeImageDriverOverrides | false;
|
||||
};
|
||||
|
||||
export type ProviderRuntimeHostSeed = {
|
||||
model: ProviderModelRuntimeContext;
|
||||
resolveExecutionDrivers: () => ProviderExecutionDrivers | undefined;
|
||||
selectModel: NativeChatDriverBase['selectModel'];
|
||||
checkParams: NativeChatDriverBase['checkParams'];
|
||||
getAttachCapability: (
|
||||
model: CopilotProviderModel,
|
||||
outputType: ModelOutputType
|
||||
) => ModelAttachmentCapability | undefined;
|
||||
getActiveProviderMiddleware: (
|
||||
execution?: CopilotProviderExecution
|
||||
) => ProviderMiddlewareConfig;
|
||||
getTools: (
|
||||
options: CopilotChatOptions,
|
||||
model: string
|
||||
) => Promise<CopilotToolSet>;
|
||||
metricLabels: (
|
||||
model: string,
|
||||
labels?: ProviderMetricLabels,
|
||||
execution?: CopilotProviderExecution
|
||||
) => ProviderMetricLabels;
|
||||
};
|
||||
|
||||
export type ProviderChatDriverPrepareInput = {
|
||||
kind: 'text' | 'streamText' | 'streamObject';
|
||||
cond: ModelConditions;
|
||||
messages: PromptMessage[];
|
||||
options: CopilotChatOptions;
|
||||
execution?: CopilotProviderExecution;
|
||||
};
|
||||
|
||||
export type ProviderChatDriver = {
|
||||
prepare: (input: {
|
||||
kind: ProviderChatDriverPrepareInput['kind'];
|
||||
cond: ProviderChatDriverPrepareInput['cond'];
|
||||
messages: ProviderChatDriverPrepareInput['messages'];
|
||||
options: ProviderChatDriverPrepareInput['options'];
|
||||
execution?: ProviderChatDriverPrepareInput['execution'];
|
||||
}) => Promise<ProviderChatDriverPrepareResult | null>;
|
||||
mapError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
type NativeProviderDriverBase = Pick<
|
||||
StructuredProviderDriver,
|
||||
'createBackendConfig' | 'mapError'
|
||||
>;
|
||||
|
||||
type ChatToolingResult = Pick<
|
||||
ProviderChatDriverPrepareResult,
|
||||
'tools' | 'middleware'
|
||||
>;
|
||||
|
||||
type NativeChatDriverBase = NativeProviderDriverBase & {
|
||||
checkParams: (input: {
|
||||
cond: ModelFullConditions;
|
||||
messages?: PromptMessage[];
|
||||
embeddings?: string[];
|
||||
options?:
|
||||
| CopilotChatOptions
|
||||
| CopilotStructuredOptions
|
||||
| CopilotImageOptions;
|
||||
withAttachment?: boolean;
|
||||
execution?: CopilotProviderExecution;
|
||||
}) => Promise<ModelFullConditions>;
|
||||
selectModel: (
|
||||
cond: ModelFullConditions,
|
||||
execution?: CopilotProviderExecution
|
||||
) => CopilotProviderModel;
|
||||
getTools?: (
|
||||
options: CopilotChatOptions,
|
||||
model: string
|
||||
) => Promise<CopilotToolSet>;
|
||||
getActiveProviderMiddleware?: (
|
||||
execution?: CopilotProviderExecution
|
||||
) => ProviderMiddlewareConfig;
|
||||
};
|
||||
|
||||
type NativeStructuredDriverOverrides = Partial<StructuredProviderDriver>;
|
||||
type NativeEmbeddingDriverOverrides = Partial<EmbeddingProviderDriver>;
|
||||
type NativeRerankDriverOverrides = Partial<RerankProviderDriver>;
|
||||
type NativeImageDriverOverrides = Partial<ImageProviderDriver>;
|
||||
|
||||
type NativeChatDriverContext = {
|
||||
input: ProviderChatDriverPrepareInput;
|
||||
outputType: ModelOutputType;
|
||||
normalizedCond: ModelFullConditions;
|
||||
model: CopilotProviderModel;
|
||||
backendConfig: LlmBackendConfig;
|
||||
protocol: LlmProtocol;
|
||||
messages: PromptMessage[];
|
||||
options: NonNullable<CopilotChatOptions>;
|
||||
execution?: CopilotProviderExecution;
|
||||
};
|
||||
|
||||
type NativeChatDriverOverrides = {
|
||||
resolveOutputType?: (
|
||||
kind: ProviderChatDriverPrepareInput['kind']
|
||||
) => ModelOutputType | null;
|
||||
withAttachment?: boolean;
|
||||
prepareMessages?: (
|
||||
context: Omit<NativeChatDriverContext, 'messages'>
|
||||
) => Awaitable<PromptMessage[]>;
|
||||
resolveTooling?: (
|
||||
context: NativeChatDriverContext
|
||||
) => Awaitable<ChatToolingResult>;
|
||||
resolveRequestOptions?: (
|
||||
context: NativeChatDriverContext
|
||||
) => Awaitable<
|
||||
Partial<
|
||||
Pick<
|
||||
ProviderChatDriverPrepareResult,
|
||||
'withAttachment' | 'attachmentCapability' | 'include' | 'reasoning'
|
||||
>
|
||||
>
|
||||
>;
|
||||
};
|
||||
|
||||
export function createNativeProviderDriverFactory(
|
||||
base: NativeProviderDriverBase
|
||||
) {
|
||||
return {
|
||||
structured(
|
||||
overrides: NativeStructuredDriverOverrides = {}
|
||||
): StructuredProviderDriver {
|
||||
return {
|
||||
createBackendConfig:
|
||||
overrides.createBackendConfig ?? base.createBackendConfig,
|
||||
mapError: overrides.mapError ?? base.mapError,
|
||||
...(overrides.prepareMessages
|
||||
? { prepareMessages: overrides.prepareMessages }
|
||||
: {}),
|
||||
...(overrides.shouldRetry
|
||||
? { shouldRetry: overrides.shouldRetry }
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
embedding(
|
||||
overrides: NativeEmbeddingDriverOverrides = {}
|
||||
): EmbeddingProviderDriver {
|
||||
return {
|
||||
createBackendConfig:
|
||||
overrides.createBackendConfig ?? base.createBackendConfig,
|
||||
mapError: overrides.mapError ?? base.mapError,
|
||||
...(overrides.defaultDimensions !== undefined
|
||||
? { defaultDimensions: overrides.defaultDimensions }
|
||||
: {}),
|
||||
...(overrides.taskType ? { taskType: overrides.taskType } : {}),
|
||||
};
|
||||
},
|
||||
rerank(overrides: NativeRerankDriverOverrides = {}): RerankProviderDriver {
|
||||
return {
|
||||
createBackendConfig:
|
||||
overrides.createBackendConfig ?? base.createBackendConfig,
|
||||
mapError: overrides.mapError ?? base.mapError,
|
||||
};
|
||||
},
|
||||
image(overrides: NativeImageDriverOverrides = {}): ImageProviderDriver {
|
||||
return {
|
||||
createBackendConfig:
|
||||
overrides.createBackendConfig ?? base.createBackendConfig,
|
||||
mapError: overrides.mapError ?? base.mapError,
|
||||
...(overrides.prepareMessages
|
||||
? { prepareMessages: overrides.prepareMessages }
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compileProviderChatDriver(
|
||||
spec: NativeProviderDriverBase & NativeChatDriverOverrides,
|
||||
base: NativeChatDriverBase
|
||||
): ProviderChatDriver {
|
||||
return {
|
||||
prepare: async (input: ProviderChatDriverPrepareInput) => {
|
||||
const options: NonNullable<CopilotChatOptions> = input.options ?? {};
|
||||
const resolvedOutputType = spec.resolveOutputType?.(input.kind);
|
||||
const outputType =
|
||||
resolvedOutputType === undefined
|
||||
? input.kind === 'streamObject'
|
||||
? ModelOutputType.Object
|
||||
: ModelOutputType.Text
|
||||
: resolvedOutputType;
|
||||
if (!outputType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedCond = await base.checkParams({
|
||||
messages: input.messages,
|
||||
cond: {
|
||||
...input.cond,
|
||||
outputType,
|
||||
},
|
||||
options,
|
||||
execution: input.execution,
|
||||
...(spec.withAttachment !== undefined
|
||||
? { withAttachment: spec.withAttachment }
|
||||
: {}),
|
||||
});
|
||||
const model = base.selectModel(normalizedCond, input.execution);
|
||||
const backendConfig = await spec.createBackendConfig(input.execution);
|
||||
const route = resolveProviderModelRoute(model, outputType);
|
||||
if (!route.protocol) {
|
||||
throw new Error(`Missing native protocol for model ${model.id}`);
|
||||
}
|
||||
const partialContext = {
|
||||
input,
|
||||
outputType,
|
||||
normalizedCond,
|
||||
model,
|
||||
backendConfig:
|
||||
route.requestLayer === backendConfig.request_layer
|
||||
? backendConfig
|
||||
: { ...backendConfig, request_layer: route.requestLayer },
|
||||
protocol: route.protocol,
|
||||
options,
|
||||
execution: input.execution,
|
||||
};
|
||||
const messages = spec.prepareMessages
|
||||
? await spec.prepareMessages(partialContext)
|
||||
: input.messages;
|
||||
const context = {
|
||||
...partialContext,
|
||||
messages,
|
||||
};
|
||||
const tooling = spec.resolveTooling
|
||||
? await spec.resolveTooling(context)
|
||||
: {
|
||||
...(base.getTools
|
||||
? { tools: await base.getTools(options, model.id) }
|
||||
: {}),
|
||||
...(base.getActiveProviderMiddleware
|
||||
? {
|
||||
middleware: base.getActiveProviderMiddleware(input.execution),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const requestOptions = spec.resolveRequestOptions
|
||||
? await spec.resolveRequestOptions(context)
|
||||
: {};
|
||||
|
||||
return {
|
||||
protocol: context.protocol,
|
||||
backendConfig: context.backendConfig,
|
||||
model: model.id,
|
||||
messages,
|
||||
...(spec.withAttachment === false ? { withAttachment: false } : {}),
|
||||
...requestOptions,
|
||||
...tooling,
|
||||
};
|
||||
},
|
||||
mapError: spec.mapError,
|
||||
};
|
||||
}
|
||||
|
||||
export function createNativeExecutionDriverSpec(
|
||||
input: ProviderDriverSpec,
|
||||
runtimeBase: NativeChatDriverBase
|
||||
): ProviderExecutionDrivers {
|
||||
const driverBase = {
|
||||
createBackendConfig: input.createBackendConfig,
|
||||
mapError: input.mapError,
|
||||
};
|
||||
const nativeDrivers = createNativeProviderDriverFactory(driverBase);
|
||||
|
||||
return {
|
||||
...(input.chat !== false
|
||||
? {
|
||||
chat: compileProviderChatDriver(
|
||||
{ ...driverBase, ...input.chat },
|
||||
runtimeBase
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(input.structured !== false
|
||||
? {
|
||||
structured: nativeDrivers.structured(input.structured ?? undefined),
|
||||
}
|
||||
: {}),
|
||||
...(input.embedding !== false
|
||||
? {
|
||||
embedding: nativeDrivers.embedding(input.embedding ?? undefined),
|
||||
}
|
||||
: {}),
|
||||
...(input.rerank !== false
|
||||
? { rerank: nativeDrivers.rerank(input.rerank ?? undefined) }
|
||||
: {}),
|
||||
...(input.image !== false
|
||||
? { image: nativeDrivers.image(input.image ?? undefined) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
AnthropicOfficialProvider,
|
||||
AnthropicVertexProvider,
|
||||
} from './anthropic';
|
||||
import { CloudflareWorkersAIProvider } from './cloudflare';
|
||||
import { FalProvider } from './fal';
|
||||
import { GeminiGenerativeProvider, GeminiVertexProvider } from './gemini';
|
||||
import { OpenAIProvider } from './openai';
|
||||
|
||||
export const CopilotProviders = [
|
||||
OpenAIProvider,
|
||||
CloudflareWorkersAIProvider,
|
||||
FalProvider,
|
||||
GeminiGenerativeProvider,
|
||||
GeminiVertexProvider,
|
||||
AnthropicOfficialProvider,
|
||||
AnthropicVertexProvider,
|
||||
];
|
||||
@@ -1,249 +0,0 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../../base';
|
||||
import type { NodeTextMiddleware, ProviderMiddlewareConfig } from '../config';
|
||||
import { ToolExecutorHost } from '../runtime/hosts/tool-executor-host';
|
||||
import { mapNativeSemanticError } from '../runtime/native-errors';
|
||||
import type { ToolLoopBackend } from '../runtime/tool/bridge';
|
||||
import type { CopilotTool, CopilotToolSet } from '../tools';
|
||||
import { resolveProviderMiddleware } from './provider-middleware';
|
||||
import {
|
||||
checkProviderParams,
|
||||
getAttachCapability as getAttachCapabilityHelper,
|
||||
matchProviderModel as matchProviderModelHelper,
|
||||
type ProviderModelRuntimeContext,
|
||||
requireProviderModelSelection,
|
||||
resolveProviderModel,
|
||||
} from './provider-model-runtime';
|
||||
import {
|
||||
type CopilotProviderExecution,
|
||||
createNativeExecutionDriverSpec,
|
||||
type ProviderDriverSpec,
|
||||
type ProviderExecutionDrivers,
|
||||
type ProviderRuntimeHostSeed,
|
||||
} from './provider-runtime-contract';
|
||||
import {
|
||||
type CopilotChatOptions,
|
||||
CopilotChatTools,
|
||||
type CopilotImageOptions,
|
||||
type CopilotModelBackendKind,
|
||||
CopilotProviderModel,
|
||||
CopilotProviderType,
|
||||
type CopilotStructuredOptions,
|
||||
type ModelAttachmentCapability,
|
||||
ModelFullConditions,
|
||||
ModelOutputType,
|
||||
type PromptMessage,
|
||||
} from './types';
|
||||
export type {
|
||||
CopilotProviderExecution,
|
||||
ProviderDriverSpec,
|
||||
ProviderExecutionDrivers,
|
||||
ProviderRuntimeHostSeed,
|
||||
} from './provider-runtime-contract';
|
||||
|
||||
@Injectable()
|
||||
export abstract class CopilotProvider<C = any> {
|
||||
protected readonly logger = new Logger(this.constructor.name);
|
||||
protected readonly MAX_STEPS = 20;
|
||||
|
||||
abstract readonly type: CopilotProviderType;
|
||||
protected abstract resolveModelBackendKind(
|
||||
execution?: CopilotProviderExecution
|
||||
): CopilotModelBackendKind;
|
||||
abstract configured(execution?: CopilotProviderExecution): boolean;
|
||||
|
||||
@Inject() protected readonly AFFiNEConfig!: Config;
|
||||
@Inject() protected readonly toolExecutorHost!: ToolExecutorHost;
|
||||
|
||||
get maxSteps() {
|
||||
return this.MAX_STEPS;
|
||||
}
|
||||
|
||||
protected resolveModelRuntimeContext(
|
||||
execution?: CopilotProviderExecution
|
||||
): ProviderModelRuntimeContext {
|
||||
return {
|
||||
type: this.type,
|
||||
backendKind: this.resolveModelBackendKind(execution),
|
||||
};
|
||||
}
|
||||
|
||||
protected get modelRuntimeContext(): ProviderModelRuntimeContext {
|
||||
return this.resolveModelRuntimeContext();
|
||||
}
|
||||
|
||||
getDriverSpec(): ProviderDriverSpec | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getExecutionDrivers(): ProviderExecutionDrivers | undefined {
|
||||
const spec = this.getDriverSpec();
|
||||
return spec ? this.createDriverSpec(spec) : undefined;
|
||||
}
|
||||
|
||||
protected createDriverSpec(
|
||||
spec: ProviderDriverSpec
|
||||
): ProviderExecutionDrivers {
|
||||
return createNativeExecutionDriverSpec(spec, {
|
||||
createBackendConfig: spec.createBackendConfig,
|
||||
mapError: error => {
|
||||
const mapped = mapNativeSemanticError(error);
|
||||
return mapped === error ? spec.mapError(error) : mapped;
|
||||
},
|
||||
checkParams: input =>
|
||||
checkProviderParams(
|
||||
this.resolveModelRuntimeContext(input.execution),
|
||||
input
|
||||
),
|
||||
selectModel: (cond, execution) =>
|
||||
requireProviderModelSelection(
|
||||
this.resolveModelRuntimeContext(execution),
|
||||
cond
|
||||
),
|
||||
getTools: this.getTools.bind(this),
|
||||
getActiveProviderMiddleware: this.getActiveProviderMiddleware.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
selectModel(
|
||||
cond: ModelFullConditions,
|
||||
execution?: CopilotProviderExecution
|
||||
): CopilotProviderModel {
|
||||
return requireProviderModelSelection(
|
||||
this.resolveModelRuntimeContext(execution),
|
||||
cond
|
||||
);
|
||||
}
|
||||
|
||||
checkParams(input: {
|
||||
cond: ModelFullConditions;
|
||||
messages?: PromptMessage[];
|
||||
embeddings?: string[];
|
||||
options?:
|
||||
| CopilotChatOptions
|
||||
| CopilotStructuredOptions
|
||||
| CopilotImageOptions;
|
||||
withAttachment?: boolean;
|
||||
execution?: CopilotProviderExecution;
|
||||
}) {
|
||||
return checkProviderParams(
|
||||
this.resolveModelRuntimeContext(input.execution),
|
||||
input
|
||||
);
|
||||
}
|
||||
|
||||
getRuntimeHostSeed(): ProviderRuntimeHostSeed {
|
||||
return {
|
||||
model: this.resolveModelRuntimeContext(),
|
||||
resolveExecutionDrivers: () => this.getExecutionDrivers(),
|
||||
selectModel: this.selectModel.bind(this),
|
||||
checkParams: this.checkParams.bind(this),
|
||||
getAttachCapability: this.getAttachCapability.bind(this),
|
||||
getActiveProviderMiddleware: this.getActiveProviderMiddleware.bind(this),
|
||||
getTools: this.getTools.bind(this),
|
||||
metricLabels: this.metricLabels.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
protected getExecutionProfile(execution?: CopilotProviderExecution) {
|
||||
return execution?.profile?.type === this.type
|
||||
? execution.profile
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getActiveProviderMiddleware(
|
||||
execution?: CopilotProviderExecution
|
||||
): ProviderMiddlewareConfig {
|
||||
return (
|
||||
this.getExecutionProfile(execution)?.middleware ??
|
||||
resolveProviderMiddleware(this.type)
|
||||
);
|
||||
}
|
||||
|
||||
metricLabels(
|
||||
model: string,
|
||||
labels: Record<string, string | number | boolean | undefined> = {},
|
||||
execution?: CopilotProviderExecution
|
||||
) {
|
||||
return {
|
||||
model,
|
||||
providerId: execution?.providerId ?? `${this.type}-default`,
|
||||
...labels,
|
||||
};
|
||||
}
|
||||
|
||||
protected get config(): C {
|
||||
return this.AFFiNEConfig.copilot.providers[this.type] as C;
|
||||
}
|
||||
|
||||
protected getConfig(execution?: CopilotProviderExecution): C {
|
||||
const profile = this.getExecutionProfile(execution);
|
||||
if (profile) {
|
||||
return profile.config as C;
|
||||
}
|
||||
return this.config;
|
||||
}
|
||||
getAttachCapability(
|
||||
model: CopilotProviderModel,
|
||||
outputType: ModelOutputType
|
||||
): ModelAttachmentCapability | undefined {
|
||||
return getAttachCapabilityHelper(model, outputType);
|
||||
}
|
||||
|
||||
// make it async to allow dynamic check available models in some providers
|
||||
async match(
|
||||
cond: ModelFullConditions = {},
|
||||
execution?: CopilotProviderExecution
|
||||
): Promise<boolean> {
|
||||
return (
|
||||
this.configured(execution) &&
|
||||
matchProviderModelHelper(this.resolveModelRuntimeContext(execution), cond)
|
||||
);
|
||||
}
|
||||
|
||||
resolveModel(
|
||||
modelId: string,
|
||||
execution?: CopilotProviderExecution
|
||||
): CopilotProviderModel | undefined {
|
||||
return resolveProviderModel(
|
||||
this.resolveModelRuntimeContext(execution),
|
||||
modelId
|
||||
);
|
||||
}
|
||||
|
||||
protected getProviderSpecificTools(
|
||||
_toolName: CopilotChatTools,
|
||||
_model: string
|
||||
): [string, CopilotTool?] | undefined {
|
||||
return;
|
||||
}
|
||||
|
||||
// use for tool use, shared between providers
|
||||
async getTools(
|
||||
options: CopilotChatOptions,
|
||||
model: string
|
||||
): Promise<CopilotToolSet> {
|
||||
this.logger.debug(`getTools: ${JSON.stringify(options?.tools ?? [])}`);
|
||||
return await this.toolExecutorHost.getTools(
|
||||
options,
|
||||
model,
|
||||
this.getProviderSpecificTools.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
createNativeAdapter(
|
||||
backend: ToolLoopBackend,
|
||||
tools: CopilotToolSet,
|
||||
nodeTextMiddleware?: NodeTextMiddleware[],
|
||||
options: {
|
||||
maxSteps?: number;
|
||||
nodeTextMiddleware?: NodeTextMiddleware[];
|
||||
} = {}
|
||||
) {
|
||||
return this.toolExecutorHost.createNativeAdapter(backend, tools, {
|
||||
...options,
|
||||
nodeTextMiddleware: nodeTextMiddleware ?? options.nodeTextMiddleware,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../../base';
|
||||
import {
|
||||
buildProviderRegistry,
|
||||
type CopilotProviderRegistry,
|
||||
type CopilotProvidersConfigInput,
|
||||
} from './provider-registry';
|
||||
|
||||
@Injectable()
|
||||
export class CopilotProviderRegistryService {
|
||||
private lastConfig?: CopilotProvidersConfigInput;
|
||||
private lastRegistry?: CopilotProviderRegistry;
|
||||
|
||||
constructor(private readonly config: Config) {}
|
||||
|
||||
getRegistry(): CopilotProviderRegistry {
|
||||
const providerConfig = this.config.copilot.providers;
|
||||
if (this.lastConfig === providerConfig && this.lastRegistry) {
|
||||
return this.lastRegistry;
|
||||
}
|
||||
|
||||
const registry = buildProviderRegistry(providerConfig);
|
||||
this.lastConfig = providerConfig;
|
||||
this.lastRegistry = registry;
|
||||
return registry;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AiPromptRole } from '@prisma/client';
|
||||
import { AiSessionMessageRole } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { JSONSchema } from '../../../base';
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
CapabilityModelCapability,
|
||||
ModelConditionsContract,
|
||||
} from '../../../native';
|
||||
import type { CopilotModelBackendKind } from '../runtime/contracts';
|
||||
import {
|
||||
type StreamObject,
|
||||
StreamObjectSchema,
|
||||
@@ -97,7 +96,6 @@ export const PromptToolsSchema = z
|
||||
|
||||
export const PromptConfigStrictSchema = z.object({
|
||||
tools: PromptToolsSchema.nullable().optional(),
|
||||
proModels: z.array(z.string()).nullable().optional(),
|
||||
// params requirements
|
||||
requireContent: z.boolean().nullable().optional(),
|
||||
requireAttachment: z.boolean().nullable().optional(),
|
||||
@@ -108,7 +106,7 @@ export const PromptConfigStrictSchema = z.object({
|
||||
presencePenalty: z.number().nullable().optional(),
|
||||
temperature: z.number().nullable().optional(),
|
||||
topP: z.number().nullable().optional(),
|
||||
maxTokens: z.number().nullable().optional(),
|
||||
maxOutputTokens: z.number().nullable().optional(),
|
||||
// fal
|
||||
modelName: z.string().nullable().optional(),
|
||||
loras: z
|
||||
@@ -132,7 +130,7 @@ export type PromptTools = z.infer<typeof PromptToolsSchema>;
|
||||
|
||||
export const EmbeddingMessage = z.array(z.string().trim().min(1)).min(1);
|
||||
|
||||
export const ChatMessageRole = Object.values(AiPromptRole) as [
|
||||
export const ChatMessageRole = Object.values(AiSessionMessageRole) as [
|
||||
'system',
|
||||
'assistant',
|
||||
'user',
|
||||
@@ -268,6 +266,8 @@ const CopilotProviderOptionsSchema = z.object({
|
||||
billingUnitId: z.string().optional(),
|
||||
taskId: z.string().optional(),
|
||||
actionId: z.string().optional(),
|
||||
builtInRouteId: z.string().optional(),
|
||||
managedTargetId: z.string().optional(),
|
||||
quotaBackedRoutesAllowed: z.boolean().optional(),
|
||||
featureKind: z
|
||||
.enum([
|
||||
@@ -380,8 +380,8 @@ export interface CopilotProviderModel {
|
||||
capabilities: ModelCapability[];
|
||||
}
|
||||
|
||||
export type { CopilotModelBackendKind };
|
||||
|
||||
export type ModelConditions = Omit<ModelConditionsContract, 'outputType'>;
|
||||
export type ModelConditions = Omit<ModelConditionsContract, 'outputType'> & {
|
||||
profileId?: string;
|
||||
};
|
||||
|
||||
export type ModelFullConditions = ModelConditionsContract;
|
||||
|
||||
Reference in New Issue
Block a user