@ai-for-dev/providers 0.1.0 → 0.3.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 +15 -1
- package/extensions/index.ts +91 -21
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -31,12 +31,26 @@ Vous pouvez aussi fournir la clé par variable d'environnement : `ILAAS_API_KEY`
|
|
|
31
31
|
|
|
32
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
33
|
|
|
34
|
-
-
|
|
34
|
+
- réglable par niveau : Pi propose `off`, `low`, `medium` et `high`, et envoie `enable_thinking` et `reasoning_effort` dans `chat_template_kwargs`. La table donne la valeur à envoyer pour chaque niveau, car chaque modèle a son vocabulaire (`xhigh` pour Qwen 3.8, `max` pour DeepSeek V4.1) ;
|
|
35
|
+
- activable (`toggle`) : le modèle ne sait que réfléchir ou non. Pi propose `off` et `high`, et passe par `chat_template_kwargs.enable_thinking` ;
|
|
35
36
|
- permanent (`always`) : Pi ne propose que `high` ;
|
|
36
37
|
- absent (`none`) : Pi n'envoie aucun paramètre de raisonnement.
|
|
37
38
|
|
|
39
|
+
Seuls `qwen-3.8-27b` et `ARES/DeepSeek-V4.1-Flash` lisent un niveau d'effort dans leur modèle de chat. Ceux de Qwen 3.6 et de Gemma 4 ne connaissent que `enable_thinking`.
|
|
40
|
+
|
|
38
41
|
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
42
|
|
|
43
|
+
## Exporter un `models.json`
|
|
44
|
+
|
|
45
|
+
Certains outils lisent un `models.json` mais ne chargent pas d'extension. C'est le cas de trysquare, qui lance pi sans extension et relaie la clé lui-même. Pour eux, le paquet écrit ses deux fournisseurs au format de `~/.pi/agent/models.json` :
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm ci
|
|
49
|
+
npm run export-models -- chemin/vers/models.json # sans chemin, le fichier sort sur stdout
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
L'export lit seulement la table de `extensions/index.ts`, sans appeler les serveurs : une même version du paquet écrit toujours le même fichier. Les modèles marqués `hidden` sont exclus, et un modèle sans `contextWindow` dans la table fait échouer l'export. Les clés restent des références (`$ILAAS_API_KEY`, `$IA4DEV_API_KEY`) que pi lit dans l'environnement. `IA4DEV_ILAAS_BASE_URL` et `IA4DEV_GRICAD_BASE_URL` changent l'URL écrite, comme pour l'extension.
|
|
53
|
+
|
|
40
54
|
## Limites
|
|
41
55
|
|
|
42
56
|
- Un fournisseur déjà défini sous le même nom dans `~/.pi/agent/models.json` prend le dessus sur celui du paquet.
|
package/extensions/index.ts
CHANGED
|
@@ -13,11 +13,19 @@ type ChatModel = Model<"openai-completions">;
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* How a model handles reasoning, measured against the real servers:
|
|
16
|
-
* - "toggle":
|
|
16
|
+
* - "toggle": chat_template_kwargs.enable_thinking switches reasoning on and off, nothing finer.
|
|
17
17
|
* - "always": always reasons and ignores every switch.
|
|
18
18
|
* - "none": no reasoning; any reasoning parameter makes the server fail.
|
|
19
|
+
* - Efforts: the chat template also reads a reasoning_effort, given here as the value
|
|
20
|
+
* to send for each of Pi's low, medium and high levels.
|
|
19
21
|
*/
|
|
20
|
-
type Thinking = "toggle" | "always" | "none";
|
|
22
|
+
type Thinking = "toggle" | "always" | "none" | Efforts;
|
|
23
|
+
|
|
24
|
+
interface Efforts {
|
|
25
|
+
low: string;
|
|
26
|
+
medium: string;
|
|
27
|
+
high: string;
|
|
28
|
+
}
|
|
21
29
|
|
|
22
30
|
interface ModelTuning {
|
|
23
31
|
contextWindow?: number;
|
|
@@ -28,7 +36,7 @@ interface ModelTuning {
|
|
|
28
36
|
hidden?: boolean;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
interface Endpoint {
|
|
39
|
+
export interface Endpoint {
|
|
32
40
|
id: string;
|
|
33
41
|
name: string;
|
|
34
42
|
baseUrl: string;
|
|
@@ -36,7 +44,7 @@ interface Endpoint {
|
|
|
36
44
|
models: Record<string, ModelTuning>;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
const ENDPOINTS: Endpoint[] = [
|
|
47
|
+
export const ENDPOINTS: Endpoint[] = [
|
|
40
48
|
{
|
|
41
49
|
id: "ia4dev-ilaas",
|
|
42
50
|
name: "ILAAS",
|
|
@@ -61,9 +69,11 @@ const ENDPOINTS: Endpoint[] = [
|
|
|
61
69
|
baseUrl: "https://ia4dev-owui.univ-grenoble-alpes.fr/api",
|
|
62
70
|
envVar: "IA4DEV_API_KEY",
|
|
63
71
|
models: {
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
|
|
72
|
+
// The chat template accepts low, medium and xhigh (its default).
|
|
73
|
+
"qwen-3.8-27b": { contextWindow: 262_144, vision: true, thinking: { low: "low", medium: "medium", high: "xhigh" } },
|
|
74
|
+
// The deployed chat template accepts low, high (its default) and max, but not medium.
|
|
75
|
+
"ARES/DeepSeek-V4.1-Flash": { contextWindow: 1_048_576, vision: true, thinking: { low: "low", medium: "high", high: "max" } },
|
|
76
|
+
"gemma-4-31b": { contextWindow: 159_488, vision: true, thinking: "toggle" },
|
|
67
77
|
},
|
|
68
78
|
},
|
|
69
79
|
];
|
|
@@ -72,11 +82,34 @@ const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
|
72
82
|
const DEFAULT_MAX_TOKENS = 32_768;
|
|
73
83
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
85
|
+
type Reasoning = Pick<ChatModel, "reasoning" | "thinkingLevelMap"> & { compat: ChatModel["compat"] };
|
|
86
|
+
|
|
87
|
+
// Pi's levels without a counterpart on the server are mapped to null, which hides them.
|
|
88
|
+
function reasoningFor(thinking: Thinking): Reasoning {
|
|
89
|
+
if (thinking === "none") return { reasoning: false, compat: {} };
|
|
90
|
+
if (thinking === "always") {
|
|
91
|
+
return { reasoning: true, thinkingLevelMap: { off: null, minimal: null, low: null, medium: null }, compat: {} };
|
|
92
|
+
}
|
|
93
|
+
if (thinking === "toggle") {
|
|
94
|
+
return {
|
|
95
|
+
reasoning: true,
|
|
96
|
+
thinkingLevelMap: { minimal: null, low: null, medium: null },
|
|
97
|
+
compat: { thinkingFormat: "qwen-chat-template" },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
reasoning: true,
|
|
102
|
+
thinkingLevelMap: { minimal: null, ...thinking },
|
|
103
|
+
compat: {
|
|
104
|
+
thinkingFormat: "chat-template",
|
|
105
|
+
chatTemplateKwargs: {
|
|
106
|
+
enable_thinking: { $var: "thinking.enabled" },
|
|
107
|
+
preserve_thinking: true,
|
|
108
|
+
reasoning_effort: { $var: "thinking.effort", omitWhenOff: true },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
80
113
|
|
|
81
114
|
interface RemoteModel {
|
|
82
115
|
id: string;
|
|
@@ -88,9 +121,9 @@ interface RemoteModel {
|
|
|
88
121
|
max_output_tokens?: number;
|
|
89
122
|
}
|
|
90
123
|
|
|
91
|
-
function toModel(endpoint: Endpoint, remote: RemoteModel): ChatModel {
|
|
124
|
+
export function toModel(endpoint: Endpoint, remote: RemoteModel): ChatModel {
|
|
92
125
|
const tuning = endpoint.models[remote.id] ?? {};
|
|
93
|
-
const
|
|
126
|
+
const reasoning = reasoningFor(tuning.thinking ?? "none");
|
|
94
127
|
const contextWindow =
|
|
95
128
|
tuning.contextWindow ?? remote.max_model_len ?? remote.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW;
|
|
96
129
|
const maxTokens = tuning.maxTokens ?? remote.max_output_tokens ?? DEFAULT_MAX_TOKENS;
|
|
@@ -101,8 +134,8 @@ function toModel(endpoint: Endpoint, remote: RemoteModel): ChatModel {
|
|
|
101
134
|
provider: endpoint.id,
|
|
102
135
|
baseUrl: endpoint.baseUrl,
|
|
103
136
|
input: tuning.vision ? ["text", "image"] : ["text"],
|
|
104
|
-
reasoning:
|
|
105
|
-
thinkingLevelMap:
|
|
137
|
+
reasoning: reasoning.reasoning,
|
|
138
|
+
thinkingLevelMap: reasoning.thinkingLevelMap,
|
|
106
139
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
107
140
|
contextWindow,
|
|
108
141
|
maxTokens: Math.min(maxTokens, contextWindow),
|
|
@@ -110,9 +143,10 @@ function toModel(endpoint: Endpoint, remote: RemoteModel): ChatModel {
|
|
|
110
143
|
// These gateways reject the newer OpenAI-only fields.
|
|
111
144
|
supportsDeveloperRole: false,
|
|
112
145
|
supportsStore: false,
|
|
113
|
-
// reasoning_effort is ignored by vLLM chat templates and crashes the
|
|
146
|
+
// A top-level reasoning_effort is ignored by vLLM chat templates and crashes the
|
|
147
|
+
// Mistral models; models that read an effort get it through the chat template.
|
|
114
148
|
supportsReasoningEffort: false,
|
|
115
|
-
...
|
|
149
|
+
...reasoning.compat,
|
|
116
150
|
},
|
|
117
151
|
};
|
|
118
152
|
}
|
|
@@ -135,11 +169,47 @@ async function fetchModels(endpoint: Endpoint, context: RefreshModelsContext): P
|
|
|
135
169
|
.map((m) => toModel(endpoint, m));
|
|
136
170
|
}
|
|
137
171
|
|
|
172
|
+
// <ID>_BASE_URL (ex: IA4DEV_ILAAS_BASE_URL) remplace l'URL par défaut, pour tester ou changer de serveur.
|
|
173
|
+
function withBaseUrl(base: Endpoint): Endpoint {
|
|
174
|
+
const urlVar = `${base.id.toUpperCase().replaceAll("-", "_")}_BASE_URL`;
|
|
175
|
+
return { ...base, baseUrl: process.env[urlVar] ?? base.baseUrl };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The same providers as a pi models.json, for tools that read that file but load no
|
|
180
|
+
* extension, such as trysquare. Built from the table alone: no server is asked, so a
|
|
181
|
+
* given version of this package always writes the same file. The key stays a `$VAR`
|
|
182
|
+
* reference that pi resolves from the environment.
|
|
183
|
+
*/
|
|
184
|
+
export function modelsJson(endpoints: Endpoint[] = ENDPOINTS) {
|
|
185
|
+
const providers: Record<string, unknown> = {};
|
|
186
|
+
for (const base of endpoints) {
|
|
187
|
+
const endpoint = withBaseUrl(base);
|
|
188
|
+
const models = Object.entries(endpoint.models)
|
|
189
|
+
.filter(([, tuning]) => !tuning.hidden)
|
|
190
|
+
.map(([id, tuning]) => {
|
|
191
|
+
// Without a server to ask, the default would pass for a measured value.
|
|
192
|
+
if (tuning.contextWindow === undefined) {
|
|
193
|
+
throw new Error(`${endpoint.name}: ${id} has no contextWindow in the table`);
|
|
194
|
+
}
|
|
195
|
+
// The provider entry already says these three.
|
|
196
|
+
const { provider, api, baseUrl, ...model } = toModel(endpoint, { id });
|
|
197
|
+
return model;
|
|
198
|
+
});
|
|
199
|
+
providers[endpoint.id] = {
|
|
200
|
+
name: endpoint.name,
|
|
201
|
+
baseUrl: endpoint.baseUrl,
|
|
202
|
+
api: "openai-completions",
|
|
203
|
+
apiKey: `$${endpoint.envVar}`,
|
|
204
|
+
models,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return { providers };
|
|
208
|
+
}
|
|
209
|
+
|
|
138
210
|
export default function (pi: ExtensionAPI) {
|
|
139
211
|
for (const base of ENDPOINTS) {
|
|
140
|
-
|
|
141
|
-
const urlVar = `${base.id.toUpperCase().replaceAll("-", "_")}_BASE_URL`;
|
|
142
|
-
const endpoint = { ...base, baseUrl: process.env[urlVar] ?? base.baseUrl };
|
|
212
|
+
const endpoint = withBaseUrl(base);
|
|
143
213
|
pi.registerProvider(
|
|
144
214
|
createProvider<"openai-completions">({
|
|
145
215
|
id: endpoint.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-for-dev/providers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pi providers for the AI4Dev course models (ILAAS and IA4Dev)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"test": "node --test 'test/*.test.ts'",
|
|
33
|
-
"typecheck": "tsc --noEmit"
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"export-models": "node scripts/export-models.ts"
|
|
34
35
|
},
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"@earendil-works/pi-ai": "^1.3.0",
|
|
45
46
|
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
46
47
|
"@types/node": "^24.13.3",
|
|
48
|
+
"typebox": "^1.4.1",
|
|
47
49
|
"typescript": "^5.9.0"
|
|
48
50
|
}
|
|
49
51
|
}
|