@mostajs/aop-providers 0.1.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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @mostajs/aop-providers
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
4
+
5
+ Configuration des **fournisseurs IA** (Claude, DeepSeek, ChatGPT, Gemini) et **recherche/sourcing** (Google, **Talordata**, Tavily, Serper, Brave) — pilotée par `.env` + **UI de configuration**. Aucun appel réseau (config seulement). Alimente `@mostajs/llm` (LLM) et les connecteurs de sourcing. Stack `mosta-aop-stack`.
package/llms.txt ADDED
@@ -0,0 +1,6 @@
1
+ # @mostajs/aop-providers — fiche LLM
2
+ > Config des fournisseurs IA (Claude/DeepSeek/ChatGPT/Gemini) & recherche/sourcing (Google, Talordata, Tavily, Serper, Brave). Pilotée .env + UI. AUCUN réseau.
3
+
4
+ ## EXPORTS
5
+ PROVIDERS (catalogue) · resolveProvider(key,env) · providersStatus(env) · firstConfigured(kind,env) · llmConfigFromEnv(env)->{dialect,apiKey,baseUrl,key} · searchConfigFromEnv(env) · providersConfigForm({env,action,kinds})->html · parseProvidersForm(form)->{ENVKEY:val}
6
+ Défaut surchargeable : AOP_LLM_PROVIDER / AOP_SEARCH_PROVIDER. Clés masquées dans status/UI. llmConfigFromEnv → à passer à @mostajs/llm. Stack mosta-aop-stack.
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@mostajs/aop-providers",
3
+ "version": "0.1.0",
4
+ "description": "Configuration des fournisseurs IA (Claude/DeepSeek/ChatGPT/Gemini) & recherche/sourcing (Google, Talordata, Tavily, Serper, Brave) — pilotée par .env + UI de config. Aucun appel réseau.",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md",
15
+ "llms.txt"
16
+ ],
17
+ "devDependencies": {
18
+ "@mostajs/mjs-unit": "^0.3.0"
19
+ },
20
+ "scripts": {
21
+ "test": "node test-scripts/unit/aop-providers.test.mjs && node examples/run.mjs"
22
+ }
23
+ }
@@ -0,0 +1,17 @@
1
+ // Dialectes fournisseur builtin — enregistrés par EFFET DE BORD (import). + meta de BENCHMARK
2
+ // (caractéristiques de référence : hébergement, vitesse, qualité, contexte, coût indicatif $/1M tokens
3
+ // ou $/req). @author Dr Hamid MADANI <drmdh@msn.com>
4
+ import { registerProviderDialect as R } from '../registry.js';
5
+ // ── IA / LLM ── meta:{ hosting, speed, quality, contextK, costIn, costOut } (coûts $/1M tokens, indicatifs)
6
+ R({ key: 'anthropic', kind: 'llm', label: 'Claude (Anthropic)', env: ['ANTHROPIC_API_KEY'], baseUrlEnv: 'ANTHROPIC_BASE_URL', baseUrl: 'https://api.anthropic.com', dashboard: 'https://console.anthropic.com', dialect: 'anthropic', meta: { hosting: 'cloud', speed: 'moyen', quality: 'très élevée', contextK: 200, costIn: 3, costOut: 15 } });
7
+ R({ key: 'deepseek', kind: 'llm', label: 'DeepSeek', env: ['DEEPSEEK_API_KEY'], baseUrlEnv: 'DEEPSEEK_BASE_URL', baseUrl: 'https://api.deepseek.com', dashboard: 'https://platform.deepseek.com', dialect: 'deepseek', meta: { hosting: 'cloud', speed: 'rapide', quality: 'élevée', contextK: 64, costIn: 0.27, costOut: 1.1 } });
8
+ R({ key: 'openai', kind: 'llm', label: 'ChatGPT (OpenAI)', env: ['OPENAI_API_KEY'], baseUrlEnv: 'OPENAI_BASE_URL', baseUrl: 'https://api.openai.com', dashboard: 'https://platform.openai.com', dialect: 'openai', meta: { hosting: 'cloud', speed: 'moyen', quality: 'très élevée', contextK: 128, costIn: 2.5, costOut: 10 } });
9
+ R({ key: 'google-gemini', kind: 'llm', label: 'Gemini (Google)', env: ['GOOGLE_API_KEY'], baseUrl: 'https://generativelanguage.googleapis.com', dashboard: 'https://aistudio.google.com', dialect: 'openai', meta: { hosting: 'cloud', speed: 'rapide', quality: 'élevée', contextK: 1000, costIn: 0.075, costOut: 0.3 } });
10
+ R({ key: 'ollama', kind: 'llm', label: 'Llama (Ollama, local)', env: [], baseUrlEnv: 'OLLAMA_BASE_URL', baseUrl: 'http://localhost:11434/v1', dashboard: 'https://ollama.com/library/llama3.1', dialect: 'ollama', model: 'llama3.1', local: true, meta: { hosting: 'local', speed: 'variable', quality: 'moyenne', contextK: 128, costIn: 0, costOut: 0 } });
11
+ R({ key: 'groq', kind: 'llm', label: 'Llama (Groq)', env: ['GROQ_API_KEY'], baseUrl: 'https://api.groq.com/openai/v1', dashboard: 'https://console.groq.com', dialect: 'openai', model: 'llama-3.3-70b-versatile', meta: { hosting: 'cloud', speed: 'très rapide', quality: 'élevée', contextK: 128, costIn: 0.59, costOut: 0.79 } });
12
+ // ── Recherche / sourcing ── meta:{ coverage, costPerK ($/1000 req indicatif), realtime }
13
+ R({ key: 'google-cse', kind: 'search', label: 'Google Programmable Search', env: ['GOOGLE_CSE_KEY', 'GOOGLE_CSE_CX'], baseUrl: 'https://www.googleapis.com/customsearch/v1', dashboard: 'https://programmablesearchengine.google.com', meta: { coverage: 'Google', costPerK: 5, realtime: true } });
14
+ R({ key: 'talordata', kind: 'search', label: 'TalorData (SerpAPI)', env: ['TALORDATA_API_KEY'], baseUrlEnv: 'TALORDATA_BASE_URL', baseUrl: 'https://serpapi.talordata.net', endpoint: '/serp/v1/request', auth: 'bearer', engine: 'google', dashboard: 'https://dashboard.talordata.com/login', meta: { coverage: 'Google SERP', costPerK: 3, realtime: true } });
15
+ R({ key: 'tavily', kind: 'search', label: 'Tavily', env: ['TAVILY_API_KEY'], baseUrl: 'https://api.tavily.com', dashboard: 'https://app.tavily.com', meta: { coverage: 'web (IA)', costPerK: 8, realtime: true } });
16
+ R({ key: 'serper', kind: 'search', label: 'Serper (Google API)', env: ['SERPER_API_KEY'], baseUrl: 'https://google.serper.dev', dashboard: 'https://serper.dev', meta: { coverage: 'Google SERP', costPerK: 1, realtime: true } });
17
+ R({ key: 'brave', kind: 'search', label: 'Brave Search', env: ['BRAVE_API_KEY'], baseUrl: 'https://api.search.brave.com', dashboard: 'https://brave.com/search/api', meta: { coverage: 'Brave (indépendant)', costPerK: 3, realtime: true } });
package/src/index.js ADDED
@@ -0,0 +1,140 @@
1
+ // @mostajs/aop-providers — config des fournisseurs IA & recherche, PATRON DIALECTE (registre, comme
2
+ // @mostajs/llm). Pilotée par .env + UI. AUCUN appel réseau. @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0
3
+ import './dialects/index.js'; // enregistre les dialectes builtin (effet de bord, comme @mostajs/llm/dialects)
4
+ import { registerProviderDialect, getProviderDialect, listProviderDialects, clearProviderDialects } from './registry.js';
5
+ export { registerProviderDialect, getProviderDialect, listProviderDialects, clearProviderDialects };
6
+
7
+ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
8
+ const mask = (v) => (v ? `${String(v).slice(0, 3)}••••${String(v).slice(-2)}` : '');
9
+
10
+ /** Liste des dialectes (back-compat : PROVIDERS reste un tableau). */
11
+ export const PROVIDERS = listProviderDialects();
12
+ export const providerByKey = (key) => getProviderDialect(key);
13
+
14
+ /** Résout la config d'un fournisseur depuis l'env (null si non configuré). */
15
+ export function resolveProvider(key, env = {}) {
16
+ const p = getProviderDialect(key); if (!p) return null;
17
+ if (!p.env.every((k) => !!env[k])) return null;
18
+ const out = { key: p.key, kind: p.kind, label: p.label, baseUrl: (p.baseUrlEnv && env[p.baseUrlEnv]) || p.baseUrl, dialect: p.dialect, local: !!p.local, endpoint: p.endpoint, engine: p.engine, meta: p.meta || {} };
19
+ for (const k of p.env) out[k.toLowerCase().replace(/_/g, '')] = env[k];
20
+ out.apiKey = p.env[0] ? env[p.env[0]] : undefined;
21
+ out.model = env.AOP_LLM_MODEL || env.LLAMA_MODEL || p.model;
22
+ return out;
23
+ }
24
+ /** État de tous les fournisseurs (clés masquées). */
25
+ export function providersStatus(env = {}) {
26
+ return listProviderDialects().map((p) => ({
27
+ key: p.key, kind: p.kind, label: p.label, dashboard: p.dashboard,
28
+ baseUrl: (p.baseUrlEnv && env[p.baseUrlEnv]) || p.baseUrl,
29
+ configured: p.env.every((k) => !!env[k]),
30
+ env: p.env.map((k) => ({ name: k, set: !!env[k], masked: mask(env[k]) })),
31
+ }));
32
+ }
33
+ export function firstConfigured(kind, env = {}) {
34
+ const p = listProviderDialects(kind).find((x) => x.env.every((k) => !!env[k]));
35
+ return p ? resolveProvider(p.key, env) : null;
36
+ }
37
+ export function llmConfigFromEnv(env = {}) {
38
+ const picked = String(env.AOP_LLM_PROVIDERS || '').split(',').map((s) => s.trim()).filter(Boolean); // sélection (cases cochées)
39
+ const order = (env.AOP_LLM_PROVIDER ? [env.AOP_LLM_PROVIDER] : []).concat(picked, ['deepseek', 'anthropic', 'openai', 'groq', 'google-gemini']);
40
+ for (const k of order) { const r = resolveProvider(k, env); if (r) return { dialect: r.dialect, apiKey: r.apiKey, baseUrl: r.baseUrl, model: r.model, key: r.key, local: r.local }; }
41
+ return null;
42
+ }
43
+ export function searchConfigFromEnv(env = {}) {
44
+ const order = (env.AOP_SEARCH_PROVIDER ? [env.AOP_SEARCH_PROVIDER] : []).concat(['talordata', 'serper', 'tavily', 'google-cse', 'brave']);
45
+ for (const k of order) { const r = resolveProvider(k, env); if (r) return r; }
46
+ return null;
47
+ }
48
+ export function providersConfigForm({ env = {}, action = '/settings/providers', kinds = ['llm', 'search'] } = {}) {
49
+ const st = providersStatus(env).filter((p) => kinds.includes(p.kind));
50
+ const group = (kind, title) => { const items = st.filter((p) => p.kind === kind); if (!items.length) return '';
51
+ return `<h2>${title}</h2>${items.map((p) => `<fieldset style="border:1px solid #e5e7eb;border-radius:8px;padding:8px 12px;margin:8px 0">
52
+ <legend>${esc(p.label)} ${p.configured ? '✅' : '⚪'} <a href="${esc(p.dashboard)}" target=_blank rel=noopener style="font-size:12px">dashboard ↗</a></legend>
53
+ ${p.env.map((e) => `<label style="display:block;margin:4px 0">${esc(e.name)} ${e.set ? `<span style="color:#16a34a;font-size:12px">(défini ${esc(e.masked)})</span>` : ''}
54
+ <input name="${esc(e.name)}" type="password" placeholder="${e.set ? '•••• (laisser vide pour garder)' : 'coller la clé…'}" style="width:100%"></label>`).join('')}
55
+ <p style="font-size:12px;color:#6b7280">endpoint : <code>${esc(p.baseUrl)}</code></p></fieldset>`).join('')}`; };
56
+ return `<form method="post" action="${esc(action)}">
57
+ <h1 id="keys">🔌 Fournisseurs IA &amp; recherche</h1>
58
+ <p class=muted>Clés stockées côté serveur (jamais exposées au client). Pilotées par <code>.env</code> ; <code>AOP_LLM_PROVIDER</code>/<code>AOP_SEARCH_PROVIDER</code> forcent le défaut.</p>
59
+ ${group('llm', '🤖 IA / LLM (scoring, go/no-go)')}${group('search', '🔎 Recherche / sourcing (veille)')}
60
+ <button>💾 Enregistrer</button></form>`;
61
+ }
62
+ export function parseProvidersForm(form = {}) {
63
+ const out = {}; for (const p of listProviderDialects()) for (const k of p.env) if (form[k] && String(form[k]).trim()) out[k] = String(form[k]).trim();
64
+ return out;
65
+ }
66
+
67
+ // ── Sélection multi-fournisseurs de recherche, triée par COÛT (moindre/nul d'abord) ──
68
+ /** [{ key, label, costPerK, free, configured, selected }] triés par coût croissant. */
69
+ export function searchSelection(env = {}) {
70
+ const sel = new Set(String(env.AOP_SEARCH_PROVIDERS || '').split(',').map((s) => s.trim()).filter(Boolean));
71
+ return listProviderDialects('search').map((d) => ({
72
+ key: d.key, label: d.label, costPerK: d.meta?.costPerK ?? null, free: (d.meta?.costPerK ?? 99) === 0,
73
+ configured: d.env.every((k) => !!env[k]), selected: sel.has(d.key),
74
+ })).sort((a, b) => (a.costPerK ?? 1e9) - (b.costPerK ?? 1e9));
75
+ }
76
+ /** Formulaire à CASES À COCHER (un ou plusieurs), coût affiché, gratuit mis en avant. */
77
+ export function searchSelectionForm({ env = {}, action = '/settings/providers', keysHref = '#keys' } = {}) {
78
+ const sel = searchSelection(env);
79
+ const ready = sel.filter((s) => s.configured).length;
80
+ const rows = sel.map((s) => `<label style="display:flex;gap:8px;align-items:center;padding:5px 0;${s.configured ? '' : 'opacity:.55'}">
81
+ <input type="checkbox" name="search_use" value="${esc(s.key)}"${s.selected ? ' checked' : ''}${s.configured ? '' : ' disabled'}>
82
+ <strong>${esc(s.label)}</strong>
83
+ <span style="font-size:12px;color:${s.free ? '#16a34a' : '#6b7280'}">${s.costPerK != null ? `${s.costPerK} $/1k req` : '—'}${s.free ? ' · GRATUIT' : ''}</span>
84
+ ${s.configured ? '<span style="font-size:12px;color:#16a34a">✓ clé OK</span>' : `<a href="${esc(keysHref)}" style="font-size:12px;color:#b45309">🔑 clé manquante — l'ajouter</a>`}</label>`).join('');
85
+ return `<form method="post" action="${esc(action)}"><h2>🔎 Fournisseurs de recherche actifs — ${ready}/${sel.length} prêts</h2>
86
+ <p class=muted>Les <strong>${sel.length}</strong> fournisseurs sont listés (triés par coût croissant). Ceux <strong>grisés</strong> n'ont pas de clé API — renseignez-la dans <em>Fournisseurs IA &amp; recherche</em> ci-dessus pour les activer. Cochez ceux à utiliser ; la découverte prend le <strong>moins cher coché</strong>.</p>
87
+ ${rows}<input type="hidden" name="_search_selection" value="1"><p><button>💾 Enregistrer la sélection</button></p></form>`;
88
+ }
89
+ /** Construit { AOP_SEARCH_PROVIDERS } depuis les clés cochées. */
90
+ export function parseSearchSelection(checkedKeys = []) {
91
+ return { AOP_SEARCH_PROVIDERS: (Array.isArray(checkedKeys) ? checkedKeys : [checkedKeys]).filter(Boolean).join(',') };
92
+ }
93
+ /** Fournisseurs de recherche sélectionnés ET configurés, triés par coût (moindre d'abord). */
94
+ export function selectedSearchProviders(env = {}) {
95
+ const keys = String(env.AOP_SEARCH_PROVIDERS || '').split(',').map((s) => s.trim()).filter(Boolean);
96
+ return keys.map((k) => resolveProvider(k, env)).filter(Boolean)
97
+ .sort((a, b) => ((getProviderDialect(a.key)?.meta?.costPerK) ?? 1e9) - ((getProviderDialect(b.key)?.meta?.costPerK) ?? 1e9));
98
+ }
99
+
100
+ // ── Sélection GÉNÉRIQUE par cases à cocher (LLM ou recherche) ──
101
+ const SEL_VAR = { search: 'AOP_SEARCH_PROVIDERS', llm: 'AOP_LLM_PROVIDERS' };
102
+ /** [{ key, label, costPerK, free, hosting, quality, configured, selected }] d'un genre, trié par coût. */
103
+ export function providerSelection(kind, env = {}) {
104
+ const sel = new Set(String(env[SEL_VAR[kind]] || '').split(',').map((s) => s.trim()).filter(Boolean));
105
+ return listProviderDialects(kind).map((d) => ({
106
+ key: d.key, label: d.label, costPerK: d.meta?.costPerK ?? null, free: (d.meta?.costPerK ?? 99) === 0,
107
+ hosting: d.meta?.hosting || (d.local ? 'local' : ''), quality: d.meta?.quality || '',
108
+ configured: d.env.every((k) => !!env[k]), selected: sel.has(d.key),
109
+ })).sort((a, b) => (a.costPerK ?? 1e9) - (b.costPerK ?? 1e9));
110
+ }
111
+ /** Formulaire CASES À COCHER pour un genre (kind='llm'|'search'). Coché = activé ; 1er coché configuré = actif. */
112
+ export function providerSelectionForm({ kind = 'search', env = {}, action = '/settings/providers', keysHref = '#keys' } = {}) {
113
+ const sel = providerSelection(kind, env); const ready = sel.filter((s) => s.configured).length;
114
+ const title = kind === 'llm' ? '🤖 Modèles IA actifs (cocher pour utiliser)' : '🔎 Fournisseurs de recherche actifs';
115
+ const note = kind === 'llm'
116
+ ? "Cochez le(s) modèle(s) à utiliser pour le scoring ; le <strong>1er coché configuré</strong> est actif (les suivants servent de repli). Décochez tout = ordre par défaut. Ex. : ne cochez que DeepSeek pour n'utiliser que lui."
117
+ : "Cochez un ou plusieurs fournisseurs ; la découverte prend le <strong>moins cher coché</strong>.";
118
+ const metaOf = (s) => kind === 'search'
119
+ ? `${s.costPerK != null ? `${s.costPerK} $/1k req` : '—'}${s.free ? ' · GRATUIT' : ''}`
120
+ : `${esc(s.hosting)}${s.quality ? ` · ${esc(s.quality)}` : ''}${s.costPerK != null ? ` · ~${s.costPerK} $/Mtok` : ''}`;
121
+ const rows = sel.map((s) => `<label style="display:flex;gap:8px;align-items:center;padding:5px 0;${s.configured ? '' : 'opacity:.55'}">
122
+ <input type="checkbox" name="${kind}_use" value="${esc(s.key)}"${s.selected ? ' checked' : ''}${s.configured ? '' : ' disabled'}>
123
+ <strong>${esc(s.label)}</strong>
124
+ <span style="font-size:12px;color:${s.free ? '#16a34a' : '#6b7280'}">${metaOf(s)}</span>
125
+ ${s.configured ? '<span style="font-size:12px;color:#16a34a">✓ clé OK</span>' : `<a href="${esc(keysHref)}" style="font-size:12px;color:#b45309">🔑 clé manquante — l'ajouter</a>`}</label>`).join('');
126
+ return `<form method="post" action="${esc(action)}"><h2>${title} — ${ready}/${sel.length} prêts</h2>
127
+ <p class=muted>${note}</p>${rows}<input type="hidden" name="_selection" value="${esc(kind)}"><p><button>💾 Enregistrer la sélection</button></p></form>`;
128
+ }
129
+ /** { AOP_<KIND>_PROVIDERS: csv } depuis les clés cochées. */
130
+ export function parseProviderSelection(kind, checkedKeys = []) {
131
+ return { [SEL_VAR[kind]]: (Array.isArray(checkedKeys) ? checkedKeys : [checkedKeys]).filter(Boolean).join(',') };
132
+ }
133
+ /** Fournisseurs d'un genre sélectionnés ET configurés, triés par coût (moindre d'abord). */
134
+ export function selectedProviders(kind, env = {}) {
135
+ const keys = String(env[SEL_VAR[kind]] || '').split(',').map((s) => s.trim()).filter(Boolean);
136
+ return keys.map((k) => resolveProvider(k, env)).filter(Boolean)
137
+ .sort((a, b) => ((getProviderDialect(a.key)?.meta?.costPerK) ?? 1e9) - ((getProviderDialect(b.key)?.meta?.costPerK) ?? 1e9));
138
+ }
139
+
140
+ export default { PROVIDERS, providerByKey, resolveProvider, providersStatus, firstConfigured, llmConfigFromEnv, searchConfigFromEnv, providersConfigForm, parseProvidersForm, searchSelection, searchSelectionForm, parseSearchSelection, selectedSearchProviders, providerSelection, providerSelectionForm, parseProviderSelection, selectedProviders, registerProviderDialect, getProviderDialect, listProviderDialects };
@@ -0,0 +1,12 @@
1
+ // @mostajs/aop-providers — registre de DIALECTES fournisseur (patron @mostajs/llm : un dialecte par
2
+ // fournisseur, enregistré par effet de bord, extensible). @author Dr Hamid MADANI <drmdh@msn.com>
3
+ const REGISTRY = new Map();
4
+ /** Enregistre un dialecte fournisseur. d = { key, kind:'llm'|'search', label, env[], baseUrl, baseUrlEnv?, endpoint?, auth?, dashboard, dialect?, model?, local?, engine? } */
5
+ export function registerProviderDialect(d) {
6
+ if (!d?.key || !d?.kind) throw new Error('provider dialect: { key, kind } requis');
7
+ REGISTRY.set(d.key, { env: [], ...d });
8
+ return d;
9
+ }
10
+ export function getProviderDialect(key) { return REGISTRY.get(key) || null; }
11
+ export function listProviderDialects(kind = null) { const all = [...REGISTRY.values()]; return kind ? all.filter((d) => d.kind === kind) : all; }
12
+ export function clearProviderDialects() { REGISTRY.clear(); }