@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.
Files changed (58) hide show
  1. package/.metadata/anthropic.response-map.json +1 -0
  2. package/.metadata/google.response-map.json +1 -0
  3. package/.metadata/groq.response-map.json +1 -0
  4. package/.metadata/llm-request-config-registry.json +111 -0
  5. package/.metadata/llm-response-config-registry.json +1 -0
  6. package/.metadata/model-aliases.json +1 -0
  7. package/.metadata/model-normalization.json +1 -0
  8. package/.metadata/moonshot.response-map.json +1 -0
  9. package/.metadata/openai.response-map.json +1 -0
  10. package/.metadata/openrouter_catalog_with_vendor_mapping.json +15781 -0
  11. package/.metadata/reasoning-support.json +159 -0
  12. package/.metadata/xai.response-map.json +1 -0
  13. package/README.md +480 -0
  14. package/dist/adapters/grok/GrokAdapter.d.ts +50 -0
  15. package/dist/adapters/grok/GrokAdapter.js +342 -0
  16. package/dist/adapters/openai/OpenAIAdapter.d.ts +50 -0
  17. package/dist/adapters/openai/OpenAIAdapter.js +401 -0
  18. package/dist/adapters/openrouter/OpenRouterAdapter.d.ts +87 -0
  19. package/dist/adapters/openrouter/OpenRouterAdapter.js +1449 -0
  20. package/dist/adapters/openrouter/reasoning-capabilities.d.ts +26 -0
  21. package/dist/adapters/openrouter/reasoning-capabilities.js +79 -0
  22. package/dist/discovery.d.ts +6 -0
  23. package/dist/discovery.js +114 -0
  24. package/dist/errors.d.ts +27 -0
  25. package/dist/errors.js +33 -0
  26. package/dist/factory.d.ts +15 -0
  27. package/dist/factory.js +206 -0
  28. package/dist/gateway.d.ts +22 -0
  29. package/dist/gateway.js +154 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.js +42 -0
  32. package/dist/interceptors.d.ts +10 -0
  33. package/dist/interceptors.js +1 -0
  34. package/dist/logger.d.ts +70 -0
  35. package/dist/logger.js +222 -0
  36. package/dist/openrouter-catalog.d.ts +119 -0
  37. package/dist/openrouter-catalog.js +222 -0
  38. package/dist/providers/OpenRouterProvider.d.ts +16 -0
  39. package/dist/providers/OpenRouterProvider.js +171 -0
  40. package/dist/registry/AdapterRegistry.d.ts +86 -0
  41. package/dist/registry/AdapterRegistry.js +36 -0
  42. package/dist/registry/ProviderRegistry.d.ts +24 -0
  43. package/dist/registry/ProviderRegistry.js +46 -0
  44. package/dist/router/Router.d.ts +33 -0
  45. package/dist/router/Router.js +258 -0
  46. package/dist/router/RouterTypes.d.ts +138 -0
  47. package/dist/router/RouterTypes.js +5 -0
  48. package/dist/router/RouterWrapper.d.ts +83 -0
  49. package/dist/router/RouterWrapper.js +744 -0
  50. package/dist/router.d.ts +13 -0
  51. package/dist/router.js +8 -0
  52. package/dist/types.d.ts +33 -0
  53. package/dist/types.js +1 -0
  54. package/dist/utils/esm-compat.d.ts +9 -0
  55. package/dist/utils/esm-compat.js +13 -0
  56. package/dist/utils/ids.d.ts +4 -0
  57. package/dist/utils/ids.js +6 -0
  58. package/package.json +66 -0
@@ -0,0 +1,401 @@
1
+ // Import ai-io-normalizer functions
2
+ // Note: Type declaration in src/types/ai-io-normalizer.d.ts allows this import
3
+ import { normalizeProviderResponse, loadResponseMaps } from 'ai-io-normalizer/dist/response-normalizer.js';
4
+ const normalizerAvailable = true;
5
+ // Lazy-load response maps (loaded once, cached)
6
+ let cachedResponseMaps = null;
7
+ function getResponseMaps() {
8
+ if (!normalizerAvailable || !loadResponseMaps) {
9
+ return null;
10
+ }
11
+ if (!cachedResponseMaps) {
12
+ try {
13
+ // Load registry - ai-io-normalizer has built-in response maps
14
+ const registry = {
15
+ schemaVersion: '1.0.0',
16
+ standard: 'chat_completions_like_v1',
17
+ providers: {
18
+ openai: { ref: 'openai' },
19
+ xai: { ref: 'xai' },
20
+ groq: { ref: 'groq' },
21
+ anthropic: { ref: 'anthropic' },
22
+ google: { ref: 'google' },
23
+ moonshot: { ref: 'moonshot' },
24
+ },
25
+ };
26
+ cachedResponseMaps = loadResponseMaps(registry);
27
+ }
28
+ catch (error) {
29
+ console.warn('[OpenAIAdapter] Failed to load response maps:', error.message);
30
+ return null;
31
+ }
32
+ }
33
+ return cachedResponseMaps;
34
+ }
35
+ /**
36
+ * Extract output text using ai-io-normalizer's normalizeProviderResponse
37
+ * This uses the same parsing logic as ai-io-normalizer adapters
38
+ */
39
+ function extractOutputTextUsingNormalizer(raw, provider, apiVariant) {
40
+ if (!raw)
41
+ return undefined;
42
+ // Try to use ai-io-normalizer if available
43
+ if (normalizerAvailable && normalizeProviderResponse) {
44
+ try {
45
+ const maps = getResponseMaps();
46
+ if (maps) {
47
+ const normalized = normalizeProviderResponse(raw, {
48
+ provider,
49
+ apiVariant,
50
+ mode: 'standard',
51
+ keepRawOverride: false,
52
+ }, maps);
53
+ // Extract text from normalized response
54
+ if (normalized.choices && normalized.choices.length > 0) {
55
+ const firstChoice = normalized.choices[0];
56
+ if (firstChoice.message?.content) {
57
+ return firstChoice.message.content;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ catch (error) {
63
+ // Fallback to manual extraction if normalizer fails
64
+ console.warn('[OpenAIAdapter] Failed to use ai-io-normalizer, falling back to manual extraction:', error.message);
65
+ }
66
+ }
67
+ // Fallback: manual extraction (same logic as before)
68
+ if (typeof raw.output_text === 'string' && raw.output_text.trim()) {
69
+ return raw.output_text;
70
+ }
71
+ const out = raw.output;
72
+ if (!Array.isArray(out) || out.length === 0)
73
+ return undefined;
74
+ const parts = [];
75
+ for (const item of out) {
76
+ const content = item?.content;
77
+ if (Array.isArray(content)) {
78
+ for (const c of content) {
79
+ if (typeof c?.text === 'string') {
80
+ parts.push(c.text);
81
+ }
82
+ else if (typeof c?.text?.value === 'string') {
83
+ parts.push(c.text.value);
84
+ }
85
+ else if (typeof c?.value === 'string') {
86
+ parts.push(c.value);
87
+ }
88
+ }
89
+ }
90
+ else if (content && typeof content === 'object') {
91
+ if (typeof content.text === 'string') {
92
+ parts.push(content.text);
93
+ }
94
+ else if (typeof content.text?.value === 'string') {
95
+ parts.push(content.text.value);
96
+ }
97
+ }
98
+ if (typeof item?.text === 'string') {
99
+ parts.push(item.text);
100
+ }
101
+ }
102
+ const joined = parts.join('');
103
+ return joined.trim() ? joined : undefined;
104
+ }
105
+ /**
106
+ * Router-side adapter for OpenAI provider
107
+ * Converts router requests to ProviderSDKCallSpec and parses responses
108
+ */
109
+ export class OpenAIAdapter {
110
+ constructor() {
111
+ this.provider = 'openai';
112
+ }
113
+ async buildCallSpec(input) {
114
+ const { requestId, mode, request, exec } = input;
115
+ // Extract model from request (could be in request.model or request.config.model)
116
+ const model = request.model || request.config?.model || 'gpt-4o-mini';
117
+ // Determine operation based on mode
118
+ const operation = mode === 'stream' ? 'openai.responses.stream' : 'openai.responses.create';
119
+ // Build OpenAI Responses API args
120
+ // OpenAI Responses API expects: { model, input: string | { input_text: string, ... } }
121
+ let inputValue;
122
+ // Handle different request formats
123
+ if (typeof request.inputData === 'string') {
124
+ // Simple string input
125
+ inputValue = request.inputData;
126
+ }
127
+ else if (request.inputData) {
128
+ // Structured input
129
+ inputValue = { input_text: String(request.inputData) };
130
+ }
131
+ else if (request.messages) {
132
+ // Convert messages to input_text
133
+ const systemMessages = request.messages.filter((m) => m.role === 'system');
134
+ const userMessages = request.messages.filter((m) => m.role === 'user');
135
+ const assistantMessages = request.messages.filter((m) => m.role === 'assistant');
136
+ const parts = [];
137
+ if (systemMessages.length > 0) {
138
+ parts.push(...systemMessages.map((m) => `System: ${m.content || ''}`));
139
+ }
140
+ if (userMessages.length > 0) {
141
+ parts.push(...userMessages.map((m) => `User: ${m.content || ''}`));
142
+ }
143
+ if (assistantMessages.length > 0) {
144
+ parts.push(...assistantMessages.map((m) => `Assistant: ${m.content || ''}`));
145
+ }
146
+ inputValue = parts.join('\n\n');
147
+ }
148
+ else if (request.instructions) {
149
+ // Use instructions as input
150
+ inputValue = request.instructions;
151
+ }
152
+ else {
153
+ // Fallback: stringify the request
154
+ inputValue = JSON.stringify(request);
155
+ }
156
+ // Build args for OpenAI Responses API
157
+ const args = {
158
+ model,
159
+ input: inputValue,
160
+ };
161
+ // Add config parameters
162
+ if (request.config) {
163
+ if (request.config.maxTokens !== undefined) {
164
+ args.max_output_tokens = request.config.maxTokens;
165
+ }
166
+ if (request.config.temperature !== undefined) {
167
+ args.temperature = request.config.temperature;
168
+ }
169
+ if (request.config.topP !== undefined) {
170
+ args.top_p = request.config.topP;
171
+ }
172
+ if (request.config.stop !== undefined) {
173
+ args.stop = request.config.stop;
174
+ }
175
+ }
176
+ return {
177
+ requestId,
178
+ provider: 'openai',
179
+ mode,
180
+ operation,
181
+ args,
182
+ exec: exec ? {
183
+ timeoutMs: exec.timeoutMs,
184
+ retries: exec.retries,
185
+ idempotencyKey: exec.idempotencyKey,
186
+ signal: exec.signal,
187
+ } : undefined,
188
+ };
189
+ }
190
+ parseResponse(input) {
191
+ const { requestId, execResult } = input;
192
+ const rawResponse = execResult.rawResponse;
193
+ // Determine API variant from operation or request
194
+ // OpenAI provider uses 'openai.responses.create' or 'openai.responses.stream' for Responses API
195
+ // or 'openai.chat.completions.create' for Chat Completions API
196
+ const operation = execResult.rawMeta?.operation;
197
+ const apiVariant = operation?.includes('responses') ? 'openai.responses' : 'openai.chat_completions';
198
+ // Use ai-io-normalizer for parsing (router stays orchestration-only)
199
+ const outputText = extractOutputTextUsingNormalizer(rawResponse, 'openai', apiVariant);
200
+ // Extract usage using ai-io-normalizer if possible, otherwise manual extraction
201
+ let usage;
202
+ if (normalizerAvailable && normalizeProviderResponse) {
203
+ try {
204
+ const maps = getResponseMaps();
205
+ if (maps) {
206
+ const normalized = normalizeProviderResponse(rawResponse, {
207
+ provider: 'openai',
208
+ apiVariant,
209
+ mode: 'standard',
210
+ keepRawOverride: false,
211
+ }, maps);
212
+ if (normalized.usage) {
213
+ usage = {
214
+ inputTokens: normalized.usage.prompt_tokens,
215
+ outputTokens: normalized.usage.completion_tokens,
216
+ totalTokens: normalized.usage.total_tokens,
217
+ };
218
+ }
219
+ }
220
+ }
221
+ catch (error) {
222
+ // Fallback to manual extraction
223
+ }
224
+ }
225
+ // Fallback to manual extraction if normalizer not available or failed
226
+ if (!usage && rawResponse?.usage) {
227
+ usage = {
228
+ inputTokens: rawResponse.usage.input_tokens || rawResponse.usage.prompt_tokens,
229
+ outputTokens: rawResponse.usage.output_tokens || rawResponse.usage.completion_tokens,
230
+ totalTokens: rawResponse.usage.total_tokens,
231
+ };
232
+ }
233
+ // Build complete response object for ai-activities storage
234
+ const fullResponse = {
235
+ requestId,
236
+ provider: 'openai',
237
+ rawResponse,
238
+ outputText,
239
+ usage,
240
+ reasoning: {
241
+ requested: { effort: 'none', visibility: 'none' },
242
+ applied: { effort: 'none', visibility: 'none' },
243
+ artifacts: {},
244
+ availability: {
245
+ supportsEffort: false,
246
+ supportsSummary: false,
247
+ supportsTrace: false,
248
+ supportsEncrypted: false,
249
+ },
250
+ },
251
+ metadata: {
252
+ model: rawResponse?.model,
253
+ id: rawResponse?.id,
254
+ status: rawResponse?.status,
255
+ ...execResult.rawMeta,
256
+ },
257
+ };
258
+ // IMPORTANT: Create a clean copy of fullResponse without circular reference
259
+ const fullResponseForActivities = {
260
+ requestId: fullResponse.requestId,
261
+ provider: fullResponse.provider,
262
+ rawResponse: fullResponse.rawResponse,
263
+ outputText: fullResponse.outputText,
264
+ usage: fullResponse.usage,
265
+ reasoning: fullResponse.reasoning,
266
+ // Don't include metadata in the copy to avoid circular reference
267
+ };
268
+ return {
269
+ ...fullResponse,
270
+ metadata: {
271
+ ...fullResponse.metadata,
272
+ // Include full response in metadata for ai-activities storage (without circular ref)
273
+ 'ai-activities-response': fullResponseForActivities,
274
+ // Include request for complete audit trail
275
+ 'ai-activities-request': input.request,
276
+ 'ai-activities-raw-response': rawResponse,
277
+ 'ai-activities-exec-meta': execResult.rawMeta,
278
+ },
279
+ };
280
+ }
281
+ parseStreamChunk(input) {
282
+ const { requestId, chunk } = input;
283
+ const raw = chunk.raw;
284
+ const events = [];
285
+ // Emit provider_raw event
286
+ events.push({
287
+ type: 'provider_raw',
288
+ requestId,
289
+ provider: 'openai',
290
+ raw,
291
+ });
292
+ // Parse OpenAI stream chunk format
293
+ // OpenAI Responses API stream format: { output: [{ content: { text_delta: "..." } }] }
294
+ if (raw?.output && Array.isArray(raw.output)) {
295
+ for (const output of raw.output) {
296
+ if (output.content?.text_delta) {
297
+ events.push({
298
+ type: 'output_text_delta',
299
+ requestId,
300
+ delta: output.content.text_delta,
301
+ });
302
+ }
303
+ }
304
+ }
305
+ return events;
306
+ }
307
+ finalizeStream(input) {
308
+ const { requestId, collected, finalRaw } = input;
309
+ // Use finalRaw if available, otherwise use last raw event
310
+ const rawResponse = finalRaw || (collected.rawEvents.length > 0 ? collected.rawEvents[collected.rawEvents.length - 1] : {});
311
+ // Build complete response object for ai-activities storage
312
+ const fullResponse = {
313
+ requestId,
314
+ provider: 'openai',
315
+ rawResponse,
316
+ outputText: collected.outputText,
317
+ reasoning: {
318
+ requested: { effort: 'none', visibility: 'none' },
319
+ applied: { effort: 'none', visibility: 'none' },
320
+ artifacts: {},
321
+ availability: {
322
+ supportsEffort: false,
323
+ supportsSummary: false,
324
+ supportsTrace: false,
325
+ supportsEncrypted: false,
326
+ },
327
+ },
328
+ metadata: {},
329
+ };
330
+ // IMPORTANT: Create a clean copy of fullResponse without circular reference
331
+ const fullResponseForActivities = {
332
+ requestId: fullResponse.requestId,
333
+ provider: fullResponse.provider,
334
+ rawResponse: fullResponse.rawResponse,
335
+ outputText: fullResponse.outputText,
336
+ reasoning: fullResponse.reasoning,
337
+ // Don't include metadata in the copy to avoid circular reference
338
+ };
339
+ // Add usage if available (may not be present in streaming responses)
340
+ if ('usage' in fullResponse && fullResponse.usage) {
341
+ fullResponseForActivities.usage = fullResponse.usage;
342
+ }
343
+ return {
344
+ ...fullResponse,
345
+ metadata: {
346
+ ...fullResponse.metadata,
347
+ // Include full response in metadata for ai-activities storage (without circular ref)
348
+ 'ai-activities-response': fullResponseForActivities,
349
+ // Include request for complete audit trail
350
+ 'ai-activities-request': input.request,
351
+ 'ai-activities-raw-response': rawResponse,
352
+ 'ai-activities-collected-events': collected.rawEvents,
353
+ },
354
+ };
355
+ }
356
+ parseBatchItem(input) {
357
+ const { requestId, item } = input;
358
+ if (item.error) {
359
+ return {
360
+ requestId,
361
+ error: item.error,
362
+ };
363
+ }
364
+ // Parse OpenAI batch item response using ai-io-normalizer
365
+ const rawResponse = item.rawResponse;
366
+ let outputText;
367
+ if (normalizerAvailable && normalizeProviderResponse) {
368
+ try {
369
+ const maps = getResponseMaps();
370
+ if (maps) {
371
+ // Default to responses API for batch (most common)
372
+ const apiVariant = 'openai.responses';
373
+ const normalized = normalizeProviderResponse(rawResponse, {
374
+ provider: 'openai',
375
+ apiVariant,
376
+ mode: 'batch',
377
+ keepRawOverride: false,
378
+ }, maps);
379
+ if (normalized.choices && normalized.choices.length > 0) {
380
+ outputText = normalized.choices[0].message?.content;
381
+ }
382
+ }
383
+ }
384
+ catch (error) {
385
+ // Fallback to manual extraction
386
+ }
387
+ }
388
+ // Fallback to manual extraction if normalizer not available or failed
389
+ if (!outputText && rawResponse?.output && Array.isArray(rawResponse.output) && rawResponse.output.length > 0) {
390
+ const firstOutput = rawResponse.output[0];
391
+ if (firstOutput.content?.text) {
392
+ outputText = firstOutput.content.text;
393
+ }
394
+ }
395
+ return {
396
+ requestId,
397
+ rawResponse,
398
+ outputText,
399
+ };
400
+ }
401
+ }
@@ -0,0 +1,87 @@
1
+ import type { ProviderSDKCallSpec, ProviderSDKExecResult, ProviderSDKStreamChunk, ProviderBatchResults } from '@x12i/ai-provider-interface';
2
+ import type { RouterAdapter } from '../../registry/AdapterRegistry.js';
3
+ import type { AIResponse, AIStreamEvent } from '../../router/RouterTypes.js';
4
+ /**
5
+ * Router-side adapter for OpenRouter provider
6
+ * Converts router requests to ProviderSDKCallSpec and parses responses
7
+ * Does NOT use ai-io-normalizer - parses OpenAI-compatible responses directly
8
+ */
9
+ export declare class OpenRouterAdapter implements RouterAdapter {
10
+ provider: string;
11
+ /**
12
+ * Normalize model name for OpenRouter using catalog data
13
+ * Uses the catalog to properly map provider + model to OpenRouter format
14
+ */
15
+ private normalizeModelName;
16
+ /**
17
+ * Map provider name to OpenRouter model prefix (legacy fallback)
18
+ */
19
+ private getProviderPrefix;
20
+ /**
21
+ * Get reasoning capabilities for a model
22
+ * Uses JSON registry for cross-vendor support detection
23
+ */
24
+ private getReasoningCapabilities;
25
+ /**
26
+ * Map unified reasoning request to provider-specific format
27
+ * Implements deterministic downgrade rules based on capabilities
28
+ * Supports both effort (OpenAI/xAI) and max_tokens (Anthropic/Gemini) modes
29
+ */
30
+ private mapReasoningToProvider;
31
+ buildCallSpec(input: {
32
+ requestId: string;
33
+ mode: 'sync' | 'stream';
34
+ request: any;
35
+ exec?: {
36
+ timeoutMs?: number;
37
+ retries?: number;
38
+ idempotencyKey?: string;
39
+ signal?: AbortSignal;
40
+ };
41
+ }): Promise<ProviderSDKCallSpec>;
42
+ parseResponse(input: {
43
+ requestId: string;
44
+ request: any;
45
+ execResult: ProviderSDKExecResult;
46
+ }): AIResponse;
47
+ /**
48
+ * Comprehensive content extraction function
49
+ * Tries all possible locations and formats to extract text content
50
+ * Returns the extracted text or undefined if not found
51
+ */
52
+ private extractContentFromResponse;
53
+ /**
54
+ * Build unified reasoning response from request info and parsed artifacts
55
+ * Implements deterministic downgrade rules based on what was actually returned
56
+ * This is the ONLY source of truth for reasoning response structure
57
+ */
58
+ private buildReasoningResponse;
59
+ /**
60
+ * Log reasoning processing summary
61
+ */
62
+ private logReasoningSummary;
63
+ parseStreamChunk(input: {
64
+ requestId: string;
65
+ request: any;
66
+ chunk: ProviderSDKStreamChunk;
67
+ }): AIStreamEvent[];
68
+ finalizeStream(input: {
69
+ requestId: string;
70
+ request: any;
71
+ collected: {
72
+ rawEvents: unknown[];
73
+ outputText?: string;
74
+ };
75
+ finalRaw?: unknown;
76
+ }): AIResponse;
77
+ parseBatchItem(input: {
78
+ requestId: string;
79
+ request: any;
80
+ item: ProviderBatchResults['items'][number];
81
+ }): {
82
+ requestId: string;
83
+ rawResponse?: unknown;
84
+ outputText?: string;
85
+ error?: any;
86
+ };
87
+ }