@ai-for-dev/providers 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +63 -2
- package/extensions/index.ts +155 -0
- package/package.json +47 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AI-for-dev team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ai-for-dev/providers
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Providers [Pi](https://pi.dev) pour la formation AI4Dev. Le paquet ajoute deux fournisseurs de modèles :
|
|
4
|
+
|
|
5
|
+
| Commande | Serveur |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `/login ia4dev-ilaas` | https://devllm.ilaas.fr/v1 |
|
|
8
|
+
| `/login ia4dev-gricad` | https://ia4dev-owui.univ-grenoble-alpes.fr/api |
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pi install npm:@ai-for-dev/providers
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Utilisation
|
|
17
|
+
|
|
18
|
+
1. Lancez `pi`.
|
|
19
|
+
2. Tapez `/login ia4dev-ilaas` (ou `/login ia4dev-gricad`) et collez votre clé API.
|
|
20
|
+
3. Choisissez un modèle avec `/model`.
|
|
21
|
+
|
|
22
|
+
La liste des modèles est lue sur le serveur (`GET /models`) avec votre clé. Elle se met à jour au démarrage, à l'ouverture de `/model` et après `/login`.
|
|
23
|
+
|
|
24
|
+
Vous pouvez aussi fournir la clé par variable d'environnement : `ILAAS_API_KEY` ou `IA4DEV_API_KEY`.
|
|
25
|
+
|
|
26
|
+
## Changer d'URL
|
|
27
|
+
|
|
28
|
+
`IA4DEV_ILAAS_BASE_URL` et `IA4DEV_GRICAD_BASE_URL` remplacent l'URL par défaut.
|
|
29
|
+
|
|
30
|
+
## Réglages des modèles
|
|
31
|
+
|
|
32
|
+
`extensions/index.ts` contient, pour chaque serveur, une table des modèles connus : taille du contexte, sortie maximale, lecture d'images et gestion du raisonnement. Ces valeurs ont été mesurées sur les serveurs. Le raisonnement peut être :
|
|
33
|
+
|
|
34
|
+
- activable (`toggle`) : Pi propose `off` et `high`, et passe par `chat_template_kwargs.enable_thinking` ;
|
|
35
|
+
- permanent (`always`) : Pi ne propose que `high` ;
|
|
36
|
+
- absent (`none`) : Pi n'envoie aucun paramètre de raisonnement.
|
|
37
|
+
|
|
38
|
+
Un modèle marqué `hidden: true` n'apparaît pas, même si le serveur le liste. Un modèle qui n'est pas dans la table reste utilisable. Il prend la taille de contexte donnée par le serveur (128 000 tokens à défaut), 32 768 tokens de sortie, sans images ni raisonnement.
|
|
39
|
+
|
|
40
|
+
## Limites
|
|
41
|
+
|
|
42
|
+
- Un fournisseur déjà défini sous le même nom dans `~/.pi/agent/models.json` prend le dessus sur celui du paquet.
|
|
43
|
+
- La liste des modèles est mise en cache. Après un changement d'URL, elle se met à jour au prochain rafraîchissement (`/model` ou `/login`).
|
|
44
|
+
|
|
45
|
+
## Développement
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm ci
|
|
49
|
+
npm run typecheck
|
|
50
|
+
npm test
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Les tests tournent hors ligne : un faux `pi` et un `fetch` simulé remplacent Pi et les serveurs.
|
|
54
|
+
|
|
55
|
+
## Publication
|
|
56
|
+
|
|
57
|
+
La publication reprend le fonctionnement de `@ai-for-dev/combo` :
|
|
58
|
+
|
|
59
|
+
1. Lancer le workflow `release-please` à la main. Il ouvre une pull request avec le `CHANGELOG.md` et la nouvelle version, calculés à partir des titres de commits (conventional commits).
|
|
60
|
+
2. Fusionner cette pull request, puis relancer `release-please`. Ce second passage crée le tag et la release GitHub.
|
|
61
|
+
3. La release déclenche `publish`, qui vérifie le code et envoie le paquet sur npm en mode « staged ».
|
|
62
|
+
4. Approuver la version depuis une machine qui peut répondre à la double authentification : `npm stage list`, puis `npm stage approve <id>`.
|
|
63
|
+
|
|
64
|
+
Le dépôt a besoin du secret `RELEASE_PLEASE` (un token GitHub qui peut écrire le contenu et les pull requests), et npm doit déclarer ce dépôt et `publish.yml` comme éditeur de confiance (trusted publisher) pour `@ai-for-dev/providers`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// The compat entry point: Pi maps the package root to it as well, and it is the one
|
|
2
|
+
// that exports openAICompletionsApi when the tests run under plain Node.
|
|
3
|
+
import {
|
|
4
|
+
createProvider,
|
|
5
|
+
envApiKeyAuth,
|
|
6
|
+
type Model,
|
|
7
|
+
openAICompletionsApi,
|
|
8
|
+
type RefreshModelsContext,
|
|
9
|
+
} from "@earendil-works/pi-ai/compat";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
type ChatModel = Model<"openai-completions">;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How a model handles reasoning, measured against the real servers:
|
|
16
|
+
* - "toggle": off by default, switched on with chat_template_kwargs.enable_thinking (vLLM).
|
|
17
|
+
* - "always": always reasons and ignores every switch.
|
|
18
|
+
* - "none": no reasoning; any reasoning parameter makes the server fail.
|
|
19
|
+
*/
|
|
20
|
+
type Thinking = "toggle" | "always" | "none";
|
|
21
|
+
|
|
22
|
+
interface ModelTuning {
|
|
23
|
+
contextWindow?: number;
|
|
24
|
+
maxTokens?: number;
|
|
25
|
+
vision?: boolean;
|
|
26
|
+
thinking?: Thinking;
|
|
27
|
+
// Listed by the server but unusable for the course.
|
|
28
|
+
hidden?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Endpoint {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
envVar: string;
|
|
36
|
+
models: Record<string, ModelTuning>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ENDPOINTS: Endpoint[] = [
|
|
40
|
+
{
|
|
41
|
+
id: "ia4dev-ilaas",
|
|
42
|
+
name: "ILAAS",
|
|
43
|
+
baseUrl: "https://devllm.ilaas.fr/v1",
|
|
44
|
+
envVar: "ILAAS_API_KEY",
|
|
45
|
+
models: {
|
|
46
|
+
"scaleway/glm-5.2": { contextWindow: 256_000, maxTokens: 16_384, vision: false, thinking: "always" },
|
|
47
|
+
"scaleway/glm-5.3": { contextWindow: 256_000, maxTokens: 16_384, vision: false, thinking: "always" },
|
|
48
|
+
"scaleway/deepseek-v4.1-flash": { contextWindow: 256_000, maxTokens: 32_768, vision: true, thinking: "always" },
|
|
49
|
+
"scaleway/deepseek-v4-flash-0731": { contextWindow: 256_000, maxTokens: 32_768, vision: false, thinking: "always" },
|
|
50
|
+
"qwen-3.6-35b-instruct": { contextWindow: 262_144, vision: true, thinking: "toggle" },
|
|
51
|
+
"gemma-4-31b": { contextWindow: 131_072, vision: true, thinking: "toggle" },
|
|
52
|
+
"llama-3.1-8b": { hidden: true },
|
|
53
|
+
"gpt-oss-120b": { hidden: true },
|
|
54
|
+
"mistral-small-4-119b": { contextWindow: 128_000, vision: true, thinking: "none" },
|
|
55
|
+
"mistral-medium-latest": { contextWindow: 128_000, vision: true, thinking: "none" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "ia4dev-gricad",
|
|
60
|
+
name: "IA4Dev",
|
|
61
|
+
baseUrl: "https://ia4dev-owui.univ-grenoble-alpes.fr/api",
|
|
62
|
+
envVar: "IA4DEV_API_KEY",
|
|
63
|
+
models: {
|
|
64
|
+
"qwen-3.8-27b": { vision: true, thinking: "toggle" },
|
|
65
|
+
"ARES/DeepSeek-V4.1-Flash": { vision: true, thinking: "toggle" },
|
|
66
|
+
"gemma-4-31b": { vision: true, thinking: "toggle" },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
72
|
+
const DEFAULT_MAX_TOKENS = 32_768;
|
|
73
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
74
|
+
|
|
75
|
+
// Effort levels mean nothing to these servers: reasoning is either on or off.
|
|
76
|
+
const THINKING_LEVELS: Record<Exclude<Thinking, "none">, ChatModel["thinkingLevelMap"]> = {
|
|
77
|
+
toggle: { minimal: null, low: null, medium: null },
|
|
78
|
+
always: { off: null, minimal: null, low: null, medium: null },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
interface RemoteModel {
|
|
82
|
+
id: string;
|
|
83
|
+
// LiteLLM tags embedding and reranking models with a mode; they cannot chat.
|
|
84
|
+
mode?: string;
|
|
85
|
+
// vLLM reports max_model_len, LiteLLM max_input_tokens and max_output_tokens.
|
|
86
|
+
max_model_len?: number;
|
|
87
|
+
max_input_tokens?: number;
|
|
88
|
+
max_output_tokens?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toModel(endpoint: Endpoint, remote: RemoteModel): ChatModel {
|
|
92
|
+
const tuning = endpoint.models[remote.id] ?? {};
|
|
93
|
+
const thinking = tuning.thinking ?? "none";
|
|
94
|
+
const contextWindow =
|
|
95
|
+
tuning.contextWindow ?? remote.max_model_len ?? remote.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW;
|
|
96
|
+
const maxTokens = tuning.maxTokens ?? remote.max_output_tokens ?? DEFAULT_MAX_TOKENS;
|
|
97
|
+
return {
|
|
98
|
+
id: remote.id,
|
|
99
|
+
name: remote.id,
|
|
100
|
+
api: "openai-completions",
|
|
101
|
+
provider: endpoint.id,
|
|
102
|
+
baseUrl: endpoint.baseUrl,
|
|
103
|
+
input: tuning.vision ? ["text", "image"] : ["text"],
|
|
104
|
+
reasoning: thinking !== "none",
|
|
105
|
+
thinkingLevelMap: thinking === "none" ? undefined : THINKING_LEVELS[thinking],
|
|
106
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
107
|
+
contextWindow,
|
|
108
|
+
maxTokens: Math.min(maxTokens, contextWindow),
|
|
109
|
+
compat: {
|
|
110
|
+
// These gateways reject the newer OpenAI-only fields.
|
|
111
|
+
supportsDeveloperRole: false,
|
|
112
|
+
supportsStore: false,
|
|
113
|
+
// reasoning_effort is ignored by vLLM chat templates and crashes the Mistral models.
|
|
114
|
+
supportsReasoningEffort: false,
|
|
115
|
+
...(thinking === "toggle" ? { thinkingFormat: "qwen-chat-template" as const } : {}),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function fetchModels(endpoint: Endpoint, context: RefreshModelsContext): Promise<ChatModel[]> {
|
|
121
|
+
const key = context.credential?.type === "api_key" ? context.credential.key : process.env[endpoint.envVar];
|
|
122
|
+
if (!context.allowNetwork || !key) return [];
|
|
123
|
+
|
|
124
|
+
const response = await fetch(`${endpoint.baseUrl}/models`, {
|
|
125
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
126
|
+
signal: AbortSignal.any([context.signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)]),
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`${endpoint.name}: GET /models a répondu ${response.status}`);
|
|
130
|
+
}
|
|
131
|
+
const body = (await response.json()) as { data?: RemoteModel[] };
|
|
132
|
+
return (body.data ?? [])
|
|
133
|
+
.filter((m) => typeof m.id === "string" && (m.mode === undefined || m.mode === "chat"))
|
|
134
|
+
.filter((m) => !endpoint.models[m.id]?.hidden)
|
|
135
|
+
.map((m) => toModel(endpoint, m));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default function (pi: ExtensionAPI) {
|
|
139
|
+
for (const base of ENDPOINTS) {
|
|
140
|
+
// <ID>_BASE_URL (ex: IA4DEV_ILAAS_BASE_URL) remplace l'URL par défaut, pour tester ou changer de serveur.
|
|
141
|
+
const urlVar = `${base.id.toUpperCase().replaceAll("-", "_")}_BASE_URL`;
|
|
142
|
+
const endpoint = { ...base, baseUrl: process.env[urlVar] ?? base.baseUrl };
|
|
143
|
+
pi.registerProvider(
|
|
144
|
+
createProvider<"openai-completions">({
|
|
145
|
+
id: endpoint.id,
|
|
146
|
+
name: endpoint.name,
|
|
147
|
+
baseUrl: endpoint.baseUrl,
|
|
148
|
+
auth: { apiKey: envApiKeyAuth(`${endpoint.name} API key`, [endpoint.envVar]) },
|
|
149
|
+
models: [],
|
|
150
|
+
fetchModels: (context) => fetchModels(endpoint, context),
|
|
151
|
+
api: openAICompletionsApi(),
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-for-dev/providers",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi providers for the AI4Dev course models (ILAAS and IA4Dev)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-package",
|
|
8
|
+
"provider",
|
|
9
|
+
"llm"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "AI-for-dev team",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/AI-for-dev/providers.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/AI-for-dev/providers/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"extensions",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./extensions/index.ts"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test 'test/*.test.ts'",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@earendil-works/pi-ai": "*",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@earendil-works/pi-ai": "^1.3.0",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
46
|
+
"@types/node": "^24.13.3",
|
|
47
|
+
"typescript": "^5.9.0"
|
|
48
|
+
}
|
|
49
|
+
}
|