@x12i/ai-providers-router 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.metadata/anthropic.response-map.json +1 -0
- package/.metadata/google.response-map.json +1 -0
- package/.metadata/groq.response-map.json +1 -0
- package/.metadata/llm-request-config-registry.json +111 -0
- package/.metadata/llm-response-config-registry.json +1 -0
- package/.metadata/model-aliases.json +1 -0
- package/.metadata/model-normalization.json +1 -0
- package/.metadata/moonshot.response-map.json +1 -0
- package/.metadata/openai.response-map.json +1 -0
- package/.metadata/openrouter_catalog_with_vendor_mapping.json +15781 -0
- package/.metadata/reasoning-support.json +159 -0
- package/.metadata/xai.response-map.json +1 -0
- package/README.md +480 -0
- package/dist/adapters/grok/GrokAdapter.d.ts +50 -0
- package/dist/adapters/grok/GrokAdapter.js +342 -0
- package/dist/adapters/openai/OpenAIAdapter.d.ts +50 -0
- package/dist/adapters/openai/OpenAIAdapter.js +401 -0
- package/dist/adapters/openrouter/OpenRouterAdapter.d.ts +87 -0
- package/dist/adapters/openrouter/OpenRouterAdapter.js +1449 -0
- package/dist/adapters/openrouter/reasoning-capabilities.d.ts +26 -0
- package/dist/adapters/openrouter/reasoning-capabilities.js +79 -0
- package/dist/discovery.d.ts +6 -0
- package/dist/discovery.js +114 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +33 -0
- package/dist/factory.d.ts +15 -0
- package/dist/factory.js +206 -0
- package/dist/gateway.d.ts +22 -0
- package/dist/gateway.js +154 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +42 -0
- package/dist/interceptors.d.ts +10 -0
- package/dist/interceptors.js +1 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.js +222 -0
- package/dist/openrouter-catalog.d.ts +119 -0
- package/dist/openrouter-catalog.js +222 -0
- package/dist/providers/OpenRouterProvider.d.ts +16 -0
- package/dist/providers/OpenRouterProvider.js +171 -0
- package/dist/registry/AdapterRegistry.d.ts +86 -0
- package/dist/registry/AdapterRegistry.js +36 -0
- package/dist/registry/ProviderRegistry.d.ts +24 -0
- package/dist/registry/ProviderRegistry.js +46 -0
- package/dist/router/Router.d.ts +33 -0
- package/dist/router/Router.js +258 -0
- package/dist/router/RouterTypes.d.ts +138 -0
- package/dist/router/RouterTypes.js +5 -0
- package/dist/router/RouterWrapper.d.ts +83 -0
- package/dist/router/RouterWrapper.js +744 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/dist/utils/esm-compat.d.ts +9 -0
- package/dist/utils/esm-compat.js +13 -0
- package/dist/utils/ids.d.ts +4 -0
- package/dist/utils/ids.js +6 -0
- package/package.json +66 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getDirname } from './utils/esm-compat.js';
|
|
4
|
+
// Get __dirname equivalent for ESM
|
|
5
|
+
const __dirname = getDirname(import.meta.url);
|
|
6
|
+
/**
|
|
7
|
+
* OpenRouter Catalog Loader
|
|
8
|
+
* Loads and caches the OpenRouter catalog data from JSON file
|
|
9
|
+
*/
|
|
10
|
+
export class OpenRouterCatalogLoader {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.catalog = null;
|
|
13
|
+
// Default path relative to this file
|
|
14
|
+
this.catalogPath = path.join(__dirname, '..', '.metadata', 'openrouter_catalog_with_vendor_mapping.json');
|
|
15
|
+
}
|
|
16
|
+
static getInstance() {
|
|
17
|
+
if (!OpenRouterCatalogLoader.instance) {
|
|
18
|
+
OpenRouterCatalogLoader.instance = new OpenRouterCatalogLoader();
|
|
19
|
+
}
|
|
20
|
+
return OpenRouterCatalogLoader.instance;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load the catalog from the JSON file
|
|
24
|
+
*/
|
|
25
|
+
async load() {
|
|
26
|
+
if (this.catalog) {
|
|
27
|
+
return this.catalog;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const data = fs.readFileSync(this.catalogPath, 'utf8');
|
|
31
|
+
this.catalog = JSON.parse(data);
|
|
32
|
+
return this.catalog;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new Error(`Failed to load OpenRouter catalog from ${this.catalogPath}: ${error}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the loaded catalog (throws if not loaded)
|
|
40
|
+
*/
|
|
41
|
+
getCatalog() {
|
|
42
|
+
if (!this.catalog) {
|
|
43
|
+
throw new Error('OpenRouter catalog not loaded. Call load() first.');
|
|
44
|
+
}
|
|
45
|
+
return this.catalog;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get all providers
|
|
49
|
+
*/
|
|
50
|
+
async getProviders() {
|
|
51
|
+
const catalog = await this.load();
|
|
52
|
+
return catalog.providers;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get all models
|
|
56
|
+
*/
|
|
57
|
+
async getModels() {
|
|
58
|
+
const catalog = await this.load();
|
|
59
|
+
return catalog.models;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Find a model by OpenRouter ID
|
|
63
|
+
*/
|
|
64
|
+
async findModelById(openrouterId) {
|
|
65
|
+
const models = await this.getModels();
|
|
66
|
+
return models.find(m => m.openrouterId === openrouterId) || null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Find models by canonical slug
|
|
70
|
+
*/
|
|
71
|
+
async findModelByCanonicalSlug(canonicalSlug) {
|
|
72
|
+
const models = await this.getModels();
|
|
73
|
+
return models.find(m => m.canonicalSlug === canonicalSlug) || null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Find models by aliases (returns all matches)
|
|
77
|
+
*/
|
|
78
|
+
async findModelsByAlias(alias) {
|
|
79
|
+
const models = await this.getModels();
|
|
80
|
+
return models.filter(m => m.aliases.includes(alias));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Find provider by slug
|
|
84
|
+
*/
|
|
85
|
+
async findProviderBySlug(slug) {
|
|
86
|
+
const providers = await this.getProviders();
|
|
87
|
+
return providers.find(p => p.slug === slug) || null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get all provider slugs
|
|
91
|
+
*/
|
|
92
|
+
async getProviderSlugs() {
|
|
93
|
+
const providers = await this.getProviders();
|
|
94
|
+
return providers.map(p => p.slug);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Find provider by vendor ID (from model.vendor.direct.vendorId)
|
|
98
|
+
* This maps vendor IDs to provider slugs for alias support
|
|
99
|
+
*/
|
|
100
|
+
async findProviderByVendorId(vendorId) {
|
|
101
|
+
const providers = await this.getProviders();
|
|
102
|
+
// Direct match with slug
|
|
103
|
+
const directMatch = providers.find(p => p.slug === vendorId);
|
|
104
|
+
if (directMatch) {
|
|
105
|
+
return directMatch;
|
|
106
|
+
}
|
|
107
|
+
// Some vendor IDs might be different from slugs, check common mappings
|
|
108
|
+
const vendorMappings = {
|
|
109
|
+
'xai': 'grok', // xAI is Grok's vendor
|
|
110
|
+
'meta': 'meta-llama', // Meta vendor maps to meta-llama provider
|
|
111
|
+
// Add more mappings as needed based on catalog data
|
|
112
|
+
};
|
|
113
|
+
const mappedSlug = vendorMappings[vendorId];
|
|
114
|
+
if (mappedSlug) {
|
|
115
|
+
return providers.find(p => p.slug === mappedSlug) || null;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get all vendor IDs that map to providers
|
|
121
|
+
*/
|
|
122
|
+
async getAllVendorIds() {
|
|
123
|
+
const models = await this.getModels();
|
|
124
|
+
const vendorIds = new Set();
|
|
125
|
+
for (const model of models) {
|
|
126
|
+
vendorIds.add(model.vendor.direct.vendorId);
|
|
127
|
+
}
|
|
128
|
+
return Array.from(vendorIds);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if a model is available
|
|
132
|
+
*/
|
|
133
|
+
async isModelAvailable(modelId) {
|
|
134
|
+
const model = await this.findModelById(modelId);
|
|
135
|
+
return model !== null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get model capabilities
|
|
139
|
+
*/
|
|
140
|
+
async getModelCapabilities(modelId) {
|
|
141
|
+
const model = await this.findModelById(modelId);
|
|
142
|
+
return model?.capabilities || null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Validate model supports a specific parameter
|
|
146
|
+
*/
|
|
147
|
+
async modelSupportsParameter(modelId, parameter) {
|
|
148
|
+
const capabilities = await this.getModelCapabilities(modelId);
|
|
149
|
+
return capabilities?.supportedParameters?.includes(parameter) || false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Infer provider from model name using catalog data
|
|
153
|
+
*/
|
|
154
|
+
async inferProviderFromModel(modelName) {
|
|
155
|
+
const models = await this.getModels();
|
|
156
|
+
// First try exact match with openrouterId
|
|
157
|
+
const exactMatch = models.find(m => m.openrouterId === modelName);
|
|
158
|
+
if (exactMatch) {
|
|
159
|
+
return exactMatch.vendor.author;
|
|
160
|
+
}
|
|
161
|
+
// Try alias match
|
|
162
|
+
const aliasMatch = models.find(m => m.aliases.includes(modelName));
|
|
163
|
+
if (aliasMatch) {
|
|
164
|
+
return aliasMatch.vendor.author;
|
|
165
|
+
}
|
|
166
|
+
// Try prefix match (e.g., "openai/gpt-4o" -> "openai")
|
|
167
|
+
if (modelName.includes('/')) {
|
|
168
|
+
const [prefix] = modelName.split('/');
|
|
169
|
+
const provider = await this.findProviderBySlug(prefix);
|
|
170
|
+
if (provider) {
|
|
171
|
+
return provider.slug;
|
|
172
|
+
}
|
|
173
|
+
// Also try vendor ID mapping for the prefix
|
|
174
|
+
const vendorProvider = await this.findProviderByVendorId(prefix);
|
|
175
|
+
if (vendorProvider) {
|
|
176
|
+
return vendorProvider.slug;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Normalize model name to OpenRouter format
|
|
183
|
+
* Returns the canonical OpenRouter ID for the model
|
|
184
|
+
*/
|
|
185
|
+
async normalizeModelName(modelName, providerHint) {
|
|
186
|
+
const models = await this.getModels();
|
|
187
|
+
// If already in OpenRouter format (contains '/'), validate it
|
|
188
|
+
if (modelName.includes('/')) {
|
|
189
|
+
const model = models.find(m => m.openrouterId === modelName);
|
|
190
|
+
if (model) {
|
|
191
|
+
return model.openrouterId;
|
|
192
|
+
}
|
|
193
|
+
// If not found but contains '/', it might be an alias, continue to check
|
|
194
|
+
}
|
|
195
|
+
// Try to find by exact alias match
|
|
196
|
+
const aliasMatch = models.find(m => m.aliases.includes(modelName));
|
|
197
|
+
if (aliasMatch) {
|
|
198
|
+
return aliasMatch.openrouterId;
|
|
199
|
+
}
|
|
200
|
+
// If provider hint provided, try to construct OpenRouter ID
|
|
201
|
+
if (providerHint) {
|
|
202
|
+
const candidateId = `${providerHint}/${modelName}`;
|
|
203
|
+
const model = models.find(m => m.openrouterId === candidateId);
|
|
204
|
+
if (model) {
|
|
205
|
+
return model.openrouterId;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Fallback: try to infer provider and construct ID
|
|
209
|
+
const inferredProvider = await this.inferProviderFromModel(modelName);
|
|
210
|
+
if (inferredProvider) {
|
|
211
|
+
const candidateId = `${inferredProvider}/${modelName}`;
|
|
212
|
+
const model = models.find(m => m.openrouterId === candidateId);
|
|
213
|
+
if (model) {
|
|
214
|
+
return model.openrouterId;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Last resort: return as-is (might be a direct model name)
|
|
218
|
+
return modelName;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Export singleton instance
|
|
222
|
+
export const openRouterCatalog = OpenRouterCatalogLoader.getInstance();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter ProviderModule wrapper using @openrouter/sdk directly
|
|
3
|
+
* This properly implements the ProviderModule interface for OpenRouter
|
|
4
|
+
*/
|
|
5
|
+
import type { ProviderModule } from '@x12i/ai-provider-interface';
|
|
6
|
+
export interface OpenRouterProviderConfig {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
baseURL?: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
httpReferer?: string;
|
|
11
|
+
xTitle?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create OpenRouter ProviderModule using @openrouter/sdk directly
|
|
15
|
+
*/
|
|
16
|
+
export declare function createOpenRouterProvider(config: OpenRouterProviderConfig): ProviderModule;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter ProviderModule wrapper using @openrouter/sdk directly
|
|
3
|
+
* This properly implements the ProviderModule interface for OpenRouter
|
|
4
|
+
*/
|
|
5
|
+
import { OpenRouter } from '@openrouter/sdk';
|
|
6
|
+
/**
|
|
7
|
+
* Create OpenRouter ProviderModule using @openrouter/sdk directly
|
|
8
|
+
*/
|
|
9
|
+
export function createOpenRouterProvider(config) {
|
|
10
|
+
const client = new OpenRouter({
|
|
11
|
+
apiKey: config.apiKey,
|
|
12
|
+
serverURL: config.baseURL || 'https://openrouter.ai/api/v1',
|
|
13
|
+
httpReferer: config.httpReferer,
|
|
14
|
+
xTitle: config.xTitle,
|
|
15
|
+
timeoutMs: config.timeoutMs,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
name: 'openrouter',
|
|
19
|
+
capabilities: {
|
|
20
|
+
modes: {
|
|
21
|
+
sync: true,
|
|
22
|
+
stream: true,
|
|
23
|
+
batch: false,
|
|
24
|
+
},
|
|
25
|
+
operations: [
|
|
26
|
+
'openai.chat.completions.create',
|
|
27
|
+
'openai.chat.completions.stream',
|
|
28
|
+
'openai.responses.create',
|
|
29
|
+
'openai.responses.stream',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
async execute(spec) {
|
|
33
|
+
const { operation, args, exec } = spec;
|
|
34
|
+
// Create AbortController for timeout
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
let timeoutId = null;
|
|
37
|
+
if (exec?.timeoutMs) {
|
|
38
|
+
timeoutId = setTimeout(() => {
|
|
39
|
+
controller.abort();
|
|
40
|
+
}, exec.timeoutMs);
|
|
41
|
+
}
|
|
42
|
+
// Use exec.signal if provided, otherwise use our timeout controller
|
|
43
|
+
const signal = exec?.signal || controller.signal;
|
|
44
|
+
try {
|
|
45
|
+
let rawResponse;
|
|
46
|
+
if (operation === 'openai.responses.create' || operation === 'openai.chat.completions.create') {
|
|
47
|
+
// Convert args to OpenRouter SDK format
|
|
48
|
+
// OpenRouter SDK only supports Chat Completions format (messages array)
|
|
49
|
+
// If adapter passed 'input' (Responses API format), convert it to messages
|
|
50
|
+
const openRouterArgs = { ...args };
|
|
51
|
+
// If args has 'input' field (Responses API), convert to messages format
|
|
52
|
+
if (openRouterArgs.input && !openRouterArgs.messages) {
|
|
53
|
+
if (typeof openRouterArgs.input === 'string') {
|
|
54
|
+
openRouterArgs.messages = [{ role: 'user', content: openRouterArgs.input }];
|
|
55
|
+
}
|
|
56
|
+
else if (openRouterArgs.input.input_text) {
|
|
57
|
+
openRouterArgs.messages = [{ role: 'user', content: openRouterArgs.input.input_text }];
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
openRouterArgs.messages = [{ role: 'user', content: JSON.stringify(openRouterArgs.input) }];
|
|
61
|
+
}
|
|
62
|
+
// Remove input field as OpenRouter doesn't support it
|
|
63
|
+
delete openRouterArgs.input;
|
|
64
|
+
}
|
|
65
|
+
// Ensure messages array exists
|
|
66
|
+
if (!openRouterArgs.messages || !Array.isArray(openRouterArgs.messages)) {
|
|
67
|
+
throw new Error('OpenRouter SDK requires messages array');
|
|
68
|
+
}
|
|
69
|
+
// Convert max_output_tokens to max_tokens if present (Responses API -> Chat Completions)
|
|
70
|
+
if (openRouterArgs.max_output_tokens !== undefined && openRouterArgs.max_tokens === undefined) {
|
|
71
|
+
openRouterArgs.max_tokens = openRouterArgs.max_output_tokens;
|
|
72
|
+
delete openRouterArgs.max_output_tokens;
|
|
73
|
+
}
|
|
74
|
+
// Use OpenRouter's chat API (OpenRouter uses OpenAI-compatible API)
|
|
75
|
+
const chatResponse = await client.chat.send({
|
|
76
|
+
...openRouterArgs,
|
|
77
|
+
stream: false,
|
|
78
|
+
}, {
|
|
79
|
+
signal,
|
|
80
|
+
});
|
|
81
|
+
rawResponse = chatResponse;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new Error(`Unsupported operation: ${operation}`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
rawResponse,
|
|
88
|
+
rawMeta: {
|
|
89
|
+
operation,
|
|
90
|
+
provider: 'openrouter',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (timeoutId) {
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async *stream(spec) {
|
|
102
|
+
const { operation, args, exec } = spec;
|
|
103
|
+
// Create AbortController for timeout
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
let timeoutId = null;
|
|
106
|
+
if (exec?.timeoutMs) {
|
|
107
|
+
timeoutId = setTimeout(() => {
|
|
108
|
+
controller.abort();
|
|
109
|
+
}, exec.timeoutMs);
|
|
110
|
+
}
|
|
111
|
+
// Use exec.signal if provided, otherwise use our timeout controller
|
|
112
|
+
const signal = exec?.signal || controller.signal;
|
|
113
|
+
try {
|
|
114
|
+
if (operation === 'openai.responses.stream' || operation === 'openai.chat.completions.stream') {
|
|
115
|
+
// Convert args to OpenRouter SDK format
|
|
116
|
+
// OpenRouter SDK only supports Chat Completions format (messages array)
|
|
117
|
+
// If adapter passed 'input' (Responses API format), convert it to messages
|
|
118
|
+
const openRouterArgs = { ...args };
|
|
119
|
+
// If args has 'input' field (Responses API), convert to messages format
|
|
120
|
+
if (openRouterArgs.input && !openRouterArgs.messages) {
|
|
121
|
+
if (typeof openRouterArgs.input === 'string') {
|
|
122
|
+
openRouterArgs.messages = [{ role: 'user', content: openRouterArgs.input }];
|
|
123
|
+
}
|
|
124
|
+
else if (openRouterArgs.input.input_text) {
|
|
125
|
+
openRouterArgs.messages = [{ role: 'user', content: openRouterArgs.input.input_text }];
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
openRouterArgs.messages = [{ role: 'user', content: JSON.stringify(openRouterArgs.input) }];
|
|
129
|
+
}
|
|
130
|
+
// Remove input field as OpenRouter doesn't support it
|
|
131
|
+
delete openRouterArgs.input;
|
|
132
|
+
}
|
|
133
|
+
// Ensure messages array exists
|
|
134
|
+
if (!openRouterArgs.messages || !Array.isArray(openRouterArgs.messages)) {
|
|
135
|
+
throw new Error('OpenRouter SDK requires messages array');
|
|
136
|
+
}
|
|
137
|
+
// Convert max_output_tokens to max_tokens if present (Responses API -> Chat Completions)
|
|
138
|
+
if (openRouterArgs.max_output_tokens !== undefined && openRouterArgs.max_tokens === undefined) {
|
|
139
|
+
openRouterArgs.max_tokens = openRouterArgs.max_output_tokens;
|
|
140
|
+
delete openRouterArgs.max_output_tokens;
|
|
141
|
+
}
|
|
142
|
+
// Use OpenRouter's streaming chat API
|
|
143
|
+
const stream = await client.chat.send({
|
|
144
|
+
...openRouterArgs,
|
|
145
|
+
stream: true,
|
|
146
|
+
}, {
|
|
147
|
+
signal,
|
|
148
|
+
});
|
|
149
|
+
for await (const chunk of stream) {
|
|
150
|
+
yield {
|
|
151
|
+
raw: chunk,
|
|
152
|
+
rawMeta: {
|
|
153
|
+
operation,
|
|
154
|
+
provider: 'openrouter',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
throw new Error(`Unsupported streaming operation: ${operation}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (timeoutId) {
|
|
165
|
+
clearTimeout(timeoutId);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ProviderSDKCallSpec, ProviderSDKExecResult, ProviderSDKStreamChunk, ProviderBatchResults } from '@x12i/ai-provider-interface';
|
|
2
|
+
import type { AIResponse, AIStreamEvent } from '../router/RouterTypes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Router-side adapter interface
|
|
5
|
+
* Adapters convert router requests to ProviderSDKCallSpec and parse raw responses
|
|
6
|
+
*/
|
|
7
|
+
export interface RouterAdapter {
|
|
8
|
+
provider: string;
|
|
9
|
+
/**
|
|
10
|
+
* Build ProviderSDKCallSpec from router request
|
|
11
|
+
*/
|
|
12
|
+
buildCallSpec(input: {
|
|
13
|
+
requestId: string;
|
|
14
|
+
mode: 'sync' | 'stream';
|
|
15
|
+
request: any;
|
|
16
|
+
exec?: {
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
retries?: number;
|
|
19
|
+
idempotencyKey?: string;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
};
|
|
22
|
+
}): ProviderSDKCallSpec | Promise<ProviderSDKCallSpec>;
|
|
23
|
+
/**
|
|
24
|
+
* Parse ProviderSDKExecResult to AIResponse
|
|
25
|
+
*/
|
|
26
|
+
parseResponse(input: {
|
|
27
|
+
requestId: string;
|
|
28
|
+
request: any;
|
|
29
|
+
execResult: ProviderSDKExecResult;
|
|
30
|
+
}): AIResponse;
|
|
31
|
+
/**
|
|
32
|
+
* Parse stream chunk to router events (optional)
|
|
33
|
+
*/
|
|
34
|
+
parseStreamChunk?(input: {
|
|
35
|
+
requestId: string;
|
|
36
|
+
request: any;
|
|
37
|
+
chunk: ProviderSDKStreamChunk;
|
|
38
|
+
}): AIStreamEvent[];
|
|
39
|
+
/**
|
|
40
|
+
* Finalize stream and return final response (optional)
|
|
41
|
+
*/
|
|
42
|
+
finalizeStream?(input: {
|
|
43
|
+
requestId: string;
|
|
44
|
+
request: any;
|
|
45
|
+
collected: {
|
|
46
|
+
rawEvents: unknown[];
|
|
47
|
+
outputText?: string;
|
|
48
|
+
};
|
|
49
|
+
finalRaw?: unknown;
|
|
50
|
+
}): AIResponse;
|
|
51
|
+
/**
|
|
52
|
+
* Parse batch item result (optional)
|
|
53
|
+
*/
|
|
54
|
+
parseBatchItem?(input: {
|
|
55
|
+
requestId: string;
|
|
56
|
+
request: any;
|
|
57
|
+
item: ProviderBatchResults['items'][number];
|
|
58
|
+
}): {
|
|
59
|
+
requestId: string;
|
|
60
|
+
rawResponse?: unknown;
|
|
61
|
+
outputText?: string;
|
|
62
|
+
error?: any;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Registry for router-side adapters
|
|
67
|
+
*/
|
|
68
|
+
export declare class AdapterRegistry {
|
|
69
|
+
private map;
|
|
70
|
+
/**
|
|
71
|
+
* Register an adapter
|
|
72
|
+
*/
|
|
73
|
+
register(adapter: RouterAdapter): void;
|
|
74
|
+
/**
|
|
75
|
+
* Get adapter for a provider
|
|
76
|
+
*/
|
|
77
|
+
get(provider: string): RouterAdapter;
|
|
78
|
+
/**
|
|
79
|
+
* Check if adapter exists for provider
|
|
80
|
+
*/
|
|
81
|
+
has(provider: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* List all registered adapter provider names
|
|
84
|
+
*/
|
|
85
|
+
list(): string[];
|
|
86
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for router-side adapters
|
|
3
|
+
*/
|
|
4
|
+
export class AdapterRegistry {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.map = new Map();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Register an adapter
|
|
10
|
+
*/
|
|
11
|
+
register(adapter) {
|
|
12
|
+
this.map.set(adapter.provider, adapter);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get adapter for a provider
|
|
16
|
+
*/
|
|
17
|
+
get(provider) {
|
|
18
|
+
const a = this.map.get(provider);
|
|
19
|
+
if (!a) {
|
|
20
|
+
throw new Error(`Adapter not registered for provider: ${provider}. Available: ${Array.from(this.map.keys()).join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
return a;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if adapter exists for provider
|
|
26
|
+
*/
|
|
27
|
+
has(provider) {
|
|
28
|
+
return this.map.has(provider);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List all registered adapter provider names
|
|
32
|
+
*/
|
|
33
|
+
list() {
|
|
34
|
+
return [...this.map.keys()];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProviderModule } from '@x12i/ai-provider-interface';
|
|
2
|
+
/**
|
|
3
|
+
* Registry for provider modules
|
|
4
|
+
* Loads and manages ProviderModule instances
|
|
5
|
+
*/
|
|
6
|
+
export declare class ProviderRegistry {
|
|
7
|
+
private map;
|
|
8
|
+
/**
|
|
9
|
+
* Register a provider module
|
|
10
|
+
*/
|
|
11
|
+
register(provider: ProviderModule): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get a provider module by name
|
|
14
|
+
*/
|
|
15
|
+
get(name: string): ProviderModule;
|
|
16
|
+
/**
|
|
17
|
+
* List all registered provider names
|
|
18
|
+
*/
|
|
19
|
+
list(): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Check if a provider is registered
|
|
22
|
+
*/
|
|
23
|
+
has(name: string): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for provider modules
|
|
3
|
+
* Loads and manages ProviderModule instances
|
|
4
|
+
*/
|
|
5
|
+
export class ProviderRegistry {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.map = new Map();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Register a provider module
|
|
11
|
+
*/
|
|
12
|
+
register(provider) {
|
|
13
|
+
if (!provider.name) {
|
|
14
|
+
throw new Error('Provider module must have a name');
|
|
15
|
+
}
|
|
16
|
+
if (!provider.capabilities) {
|
|
17
|
+
throw new Error(`Provider '${provider.name}' must declare capabilities`);
|
|
18
|
+
}
|
|
19
|
+
if (!provider.execute) {
|
|
20
|
+
throw new Error(`Provider '${provider.name}' must implement execute()`);
|
|
21
|
+
}
|
|
22
|
+
this.map.set(provider.name, provider);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get a provider module by name
|
|
26
|
+
*/
|
|
27
|
+
get(name) {
|
|
28
|
+
const p = this.map.get(name);
|
|
29
|
+
if (!p) {
|
|
30
|
+
throw new Error(`Provider not registered: ${name}. Available: ${Array.from(this.map.keys()).join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* List all registered provider names
|
|
36
|
+
*/
|
|
37
|
+
list() {
|
|
38
|
+
return [...this.map.keys()];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a provider is registered
|
|
42
|
+
*/
|
|
43
|
+
has(name) {
|
|
44
|
+
return this.map.has(name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProviderRegistry } from '../registry/ProviderRegistry.js';
|
|
2
|
+
import type { AdapterRegistry } from '../registry/AdapterRegistry.js';
|
|
3
|
+
import type { AIRouterRequest, AIResponse, AIStreamEvent, AIBatchResponse, AIBatchRequestItem } from './RouterTypes.js';
|
|
4
|
+
/**
|
|
5
|
+
* Main router class
|
|
6
|
+
* Orchestrates provider execution using ProviderModules and router-side adapters
|
|
7
|
+
*/
|
|
8
|
+
export declare class AIRouter {
|
|
9
|
+
private providers;
|
|
10
|
+
private adapters;
|
|
11
|
+
constructor(providers: ProviderRegistry, adapters: AdapterRegistry);
|
|
12
|
+
/**
|
|
13
|
+
* Resolve provider name from request, checking OpenRouter mode first
|
|
14
|
+
*/
|
|
15
|
+
private resolveProviderName;
|
|
16
|
+
/**
|
|
17
|
+
* Execute a sync request
|
|
18
|
+
*/
|
|
19
|
+
runSync(input: AIRouterRequest): Promise<AIResponse>;
|
|
20
|
+
/**
|
|
21
|
+
* Execute a streaming request
|
|
22
|
+
*/
|
|
23
|
+
runStream(input: AIRouterRequest): AsyncIterable<AIStreamEvent>;
|
|
24
|
+
/**
|
|
25
|
+
* Execute a batch request
|
|
26
|
+
*/
|
|
27
|
+
runBatch(providerName: string, items: AIBatchRequestItem[], exec?: {
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
retries?: number;
|
|
30
|
+
idempotencyKey?: string;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}): Promise<AIBatchResponse>;
|
|
33
|
+
}
|