pi-provider-swiss-ai-platform 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/index.ts +460 -0
  4. package/package.json +8 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (C) 2026 Daniel Roethlisberger <daniel@roe.ch>
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 ADDED
@@ -0,0 +1,82 @@
1
+ # Pi provider extension for Swiss AI Platform
2
+ Copyright (C) 2026, [Daniel Roethlisberger](//daniel.roe.ch/).
3
+ https://github.com/droe/pi-provider-swiss-ai-platform
4
+
5
+ Experimental Pi extension that registers a model provider for [Swiss AI
6
+ Platform][1] by Swisscom in partnership with NVIDIA, offered via Swisscom's API
7
+ platform [Digital Marketplace][2].
8
+
9
+ [1]: https://www.swisscom.ch/en/business/enterprise/offer/platforms-applications/data-driven-business/swiss-ai-platform.html
10
+ [2]: https://digital.swisscom.com/products/swiss-ai-platform/info
11
+
12
+ ## Models
13
+
14
+ As of August 2026, the following models offered as part of Swiss AI Platform
15
+ work with Pi:
16
+
17
+ - `swiss-ai/Apertus-v1.5-70B`
18
+ - `google/gemma-4-31b-it`
19
+ - `qwen/qwen3.5-397b-a17b`
20
+ - `qwen/qwen3.6-35b-a3b`
21
+ - `mistralai/mistral-small-4-119b-2603` (partially)
22
+
23
+ See `MODEL_METADATA` in `index.ts` for metadata on all models, including hidden
24
+ models and why they have been hidden. Feedback or patches to improve model
25
+ compatibility very welcome.
26
+
27
+ Models added more recently, i.e. models not included in `MODEL_METADATA` yet,
28
+ may or may not work, and likely need manual configuration in
29
+ `~/.pi/agent/models.json`.
30
+
31
+ ## Prerequisites
32
+
33
+ You need a Swiss AI Platform subscription and matching Client ID and Client
34
+ Secret.
35
+
36
+ In your subscription on Digital Marketplace, select Documentation and check
37
+ «Production Url». The last path component is the subscription name, e.g.
38
+ `all-models` or `apertus-1.5-70b`. Note that for model-specific subscriptions,
39
+ the subscription name is different from the model identifier.
40
+
41
+ The credentials are more straightforward. From your subscription, select
42
+ «Credentials» and check the «OAuth 2.0 Credentials» section.
43
+
44
+ ## Installation
45
+
46
+ ```
47
+ pi install npm:pi-provider-swiss-ai-platform
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ Interactive configuration as usual: `/login` -> «Swiss AI Platform» prompts for
53
+ the subscription name, the Client ID and the Client Secret, verifies them
54
+ against the gateway and stores them in `~/.pi/agent/auth.json`. Access tokens
55
+ are minted on demand and cached in memory until they expire.
56
+
57
+ Multiple configured subscriptions are not currently implemented. Switching
58
+ subscription is another `/login`.
59
+
60
+ For headless use, the following environment variables can be set:
61
+
62
+ - `SWISS_AI_PLATFORM_SUBSCRIPTION`
63
+ - `SWISS_AI_PLATFORM_CLIENT_ID`
64
+ - `SWISS_AI_PLATFORM_CLIENT_SECRET`
65
+
66
+ ## Implementation Details
67
+
68
+ Models are served over the OpenAI-compatible `/v1/chat/completions` API; only
69
+ some of them also support `/v1/responses`, so completions is the default and
70
+ `MODEL_METADATA.api` opts a model into the Responses API. Authentication is an
71
+ OAuth 2.0 client-credentials grant (HTTP Basic client authentication).
72
+
73
+ The model catalog is pulled from `/v1/models` on model refresh and cached by Pi
74
+ in `~/.pi/agent/models-store.json`. The endpoint returns ids only, so model
75
+ capabilities come from the curated `MODEL_METADATA` table in `index.ts`.
76
+ Unknown ids fall back to `DEFAULT_METADATA`; entries flagged `hideModel: true`
77
+ are dropped from the catalog. Per-model overrides are still possible in
78
+ `~/.pi/agent/models.json`.
79
+
80
+ ## Disclaimer
81
+
82
+ This Pi extension is inofficial, neither provided by nor supported by Swisscom.
package/index.ts ADDED
@@ -0,0 +1,460 @@
1
+ /*
2
+ * Pi provider extension for Swiss AI Platform by Swisscom.
3
+ * Copyright (C) 2026 Daniel Roethlisberger <daniel@roe.ch>
4
+ * https://github.com/droe/pi-provider-swiss-ai-platform
5
+ */
6
+
7
+ import {
8
+ type ApiKeyAuth,
9
+ type ApiKeyCredential,
10
+ type AuthInteraction,
11
+ createProvider,
12
+ type Model,
13
+ type RefreshModelsContext,
14
+ } from "@earendil-works/pi-ai";
15
+ import { openAICompletionsApi, openAIResponsesApi } from "@earendil-works/pi-ai/compat";
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
+
18
+ // =============================================================================
19
+ // Configuration
20
+ // =============================================================================
21
+
22
+ const PROVIDER_ID = "swiss-ai-platform";
23
+ const PROVIDER_NAME = "Swiss AI Platform";
24
+ const PRODUCT_URL = "https://api.swisscom.com/products/swiss-ai-platform";
25
+ const TOKEN_URL = "https://api.swisscom.com/products/oauth2/token";
26
+
27
+ const ENV_SUBSCRIPTION = "SWISS_AI_PLATFORM_SUBSCRIPTION";
28
+ const ENV_CLIENT_ID = "SWISS_AI_PLATFORM_CLIENT_ID";
29
+ const ENV_CLIENT_SECRET = "SWISS_AI_PLATFORM_CLIENT_SECRET";
30
+ const ENV_ACCESS_TOKEN = "SWISS_AI_PLATFORM_ACCESS_TOKEN";
31
+
32
+ /** Safety margin subtracted from the token lifetime reported by the gateway. */
33
+ const TOKEN_EXPIRY_SKEW_MS = 60_000;
34
+
35
+ /** e.g. subscription "all-models" -> ".../swiss-ai-platform/all-models/v1" */
36
+ const baseUrlFor = (subscription: string) => `${PRODUCT_URL}/${encodeURIComponent(subscription)}/v1`;
37
+
38
+ type SwissApi = "openai-completions" | "openai-responses";
39
+ type SwissModel = Model<SwissApi>;
40
+
41
+ /** Only some models support the Responses API, so chat completions is the default. */
42
+ const DEFAULT_API: SwissApi = "openai-completions";
43
+
44
+ interface ModelMetadata {
45
+ name?: string;
46
+ /** Streaming API for this model. Default: DEFAULT_API ("openai-completions"). */
47
+ api?: SwissApi;
48
+ /**
49
+ * Keep the model out of pi entirely. Use it for models that cannot serve a
50
+ * coding agent, e.g. text-to-speech, speech-to-text, embeddings or moderation.
51
+ */
52
+ hideModel?: boolean;
53
+ reasoning?: boolean;
54
+ input?: ("text" | "image")[];
55
+ /** USD per million tokens. */
56
+ cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
57
+ contextWindow?: number;
58
+ maxTokens?: number;
59
+ thinkingLevelMap?: SwissModel["thinkingLevelMap"];
60
+ compat?: SwissModel["compat"];
61
+ }
62
+
63
+ /**
64
+ * Curated capabilities per model id, keyed exactly as returned by /v1/models.
65
+ * Model documentation by Swisscom:
66
+ * https://docs.cloud.swisscom.ch/guide/cloud-services/aip/use/inference-endpoints/
67
+ *
68
+ * Configuration may not be perfect, please submit issues or PRs with improvements.
69
+ */
70
+ const MODEL_METADATA: Record<string, ModelMetadata> = {
71
+ "google/gemma-4-31b-it": {
72
+ name: "Gemma 4 31b",
73
+ api: "openai-responses",
74
+ reasoning: true,
75
+ input: ["text", "image"],
76
+ contextWindow: 256000,
77
+ },
78
+ "magpie-tts-multilingual": {
79
+ // Disabled because Pi does not do text-to-speech.
80
+ hideModel: true,
81
+ name: "Magpie TTS Multilingual",
82
+ },
83
+ "meta/llama-3.1-8b-instruct": {
84
+ // Disabled because the model does not seem to work with Pi.
85
+ hideModel: true,
86
+ name: "Llama 3.1 8b instruct",
87
+ },
88
+ "meta/llama-4-scout-17b-16e-instruct": {
89
+ // Disabled because tool calling does not seem to work.
90
+ hideModel: true,
91
+ name: "Llama 4 Scout 17b 16e instruct",
92
+ input: ["text", "image"],
93
+ contextWindow: 131072,
94
+ },
95
+ "mistralai/mistral-small-4-119b-2603": {
96
+ // FIXME Docs state that this model supports the responses API, but
97
+ // responses API results in error 400 "'input_text' is not a valid
98
+ // ChunkTypes". Use completions until someone figures this one out.
99
+ // FIXME [THINK]...[/THINK] tags are not recognised by Pi.
100
+ name: "Mistral Small 4 119b 2603",
101
+ //api: "openai-responses",
102
+ reasoning: true,
103
+ input: ["text", "image"],
104
+ contextWindow: 256144,
105
+ thinkingLevelMap: {
106
+ off: "none",
107
+ minimal: null,
108
+ low: null,
109
+ medium: null,
110
+ high: "high",
111
+ xhigh: null,
112
+ max: null,
113
+ },
114
+ compat: {
115
+ supportsDeveloperRole: false,
116
+ },
117
+ },
118
+ "nvidia/llama-3.1-nemoguard-8b-content-safety": {
119
+ // Disabled because Pi does not do content safety.
120
+ hideModel: true,
121
+ name: "Llama 3.1 Nemoguard 8b content safety",
122
+ },
123
+ "nvidia/llama-3.2-nv-embedqa-1b-v2": {
124
+ // Disabled because Pi does not do text-to-embeddings.
125
+ hideModel: true,
126
+ name: "Llama 3.2 NV embedqa 1b v2",
127
+ },
128
+ "openai/gpt-oss-20b": {
129
+ // FIXME Works, but performs very poorly with tool calling.
130
+ // Should investigate root cause.
131
+ hideModel: true,
132
+ name: "GPT OSS 20b",
133
+ input: ["text"],
134
+ contextWindow: 131072,
135
+ },
136
+ "openai/gpt-oss-120b": {
137
+ // FIXME Works, but performs very poorly with tool calling.
138
+ // Should investigate root cause.
139
+ hideModel: true,
140
+ name: "GPT OSS 120b",
141
+ input: ["text"],
142
+ contextWindow: 131072,
143
+ },
144
+ "openai/whisper-large-v3-turbo": {
145
+ // Disabled because Pi does not do speech-to-text.
146
+ hideModel: true,
147
+ name: "Whisper Large V3 Turbo",
148
+ },
149
+ "qwen/qwen3.5-397b-a17b": {
150
+ name: "Qwen 3.5 397b A17B",
151
+ reasoning: true,
152
+ input: ["text"],
153
+ contextWindow: 262144,
154
+ thinkingLevelMap: {
155
+ off: "none",
156
+ minimal: null,
157
+ low: "low",
158
+ medium: "medium",
159
+ high: "high",
160
+ xhigh: null,
161
+ max: null,
162
+ },
163
+ compat: {
164
+ thinkingFormat: "qwen",
165
+ supportsDeveloperRole: false,
166
+ },
167
+ },
168
+ "qwen/qwen3.6-35b-a3b": {
169
+ // FIXME Docs state that this model supports the responses API, but
170
+ // responses API results in "Error: OpenAI API error (429): 429
171
+ // status code (no body)". Use completions until someone figures
172
+ // this one out.
173
+ name: "Qwen 3.6 35b A3B",
174
+ //api: "openai-responses",
175
+ reasoning: true,
176
+ input: ["text"],
177
+ contextWindow: 262144,
178
+ compat: {
179
+ thinkingFormat: "qwen",
180
+ },
181
+ },
182
+ "rednote-hilab/dots.ocr": {
183
+ // Disabled because Pi does not do OCR.
184
+ hideModel: true,
185
+ name: "Rednote Hilab Dots OCR",
186
+ },
187
+ "swiss-ai/Apertus-v1.5-70B": {
188
+ name: "Apertus v1.5 70b",
189
+ api: "openai-responses",
190
+ input: ["text", "image"],
191
+ contextWindow: 262144,
192
+ },
193
+ };
194
+
195
+ /** Applied to models returned by /v1/models that have no MODEL_METADATA entry. */
196
+ const DEFAULT_METADATA: Required<Pick<ModelMetadata, "reasoning" | "input" | "cost" | "contextWindow" | "maxTokens">> = {
197
+ reasoning: false,
198
+ input: ["text"],
199
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
200
+ contextWindow: 131_072,
201
+ maxTokens: 16_384,
202
+ };
203
+
204
+ /** Models flagged with `hideModel` are never offered by this provider. */
205
+ const isHidden = (id: string) => MODEL_METADATA[id]?.hideModel === true;
206
+
207
+ // =============================================================================
208
+ // Gateway requests
209
+ // =============================================================================
210
+
211
+ interface AccessToken {
212
+ access: string;
213
+ /** Absolute expiry in ms, already reduced by TOKEN_EXPIRY_SKEW_MS. */
214
+ expires: number;
215
+ }
216
+
217
+ async function errorDetail(response: Response): Promise<string> {
218
+ const body = await response.text().catch(() => "");
219
+ try {
220
+ const json = JSON.parse(body) as { message?: string; error_description?: string; error?: string };
221
+ return json.message ?? json.error_description ?? json.error ?? body;
222
+ } catch {
223
+ return body;
224
+ }
225
+ }
226
+
227
+ /** OAuth 2.0 client-credentials grant with HTTP Basic client authentication. */
228
+ async function requestAccessToken(clientId: string, clientSecret: string, signal?: AbortSignal): Promise<AccessToken> {
229
+ const response = await fetch(TOKEN_URL, {
230
+ method: "POST",
231
+ headers: {
232
+ Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`, "utf8").toString("base64")}`,
233
+ "Content-Type": "application/x-www-form-urlencoded",
234
+ Accept: "application/json",
235
+ },
236
+ body: new URLSearchParams({ grant_type: "client_credentials" }).toString(),
237
+ signal,
238
+ });
239
+
240
+ if (!response.ok) {
241
+ throw new Error(`${PROVIDER_NAME} token request failed (${response.status}): ${await errorDetail(response)}`);
242
+ }
243
+
244
+ const data = (await response.json()) as { access_token?: string; expires_in?: number };
245
+ if (!data.access_token) throw new Error(`${PROVIDER_NAME} token response contained no access_token`);
246
+
247
+ return {
248
+ access: data.access_token,
249
+ expires: Date.now() + (data.expires_in ?? 3600) * 1000 - TOKEN_EXPIRY_SKEW_MS,
250
+ };
251
+ }
252
+
253
+ /** In-process token cache; promises are cached so concurrent requests share one grant. */
254
+ const tokenCache = new Map<string, Promise<AccessToken>>();
255
+
256
+ async function cachedAccessToken(clientId: string, clientSecret: string, signal?: AbortSignal): Promise<string> {
257
+ const key = `${clientId}:${clientSecret}`;
258
+ const pending = tokenCache.get(key);
259
+
260
+ if (pending) {
261
+ const token = await pending.catch(() => undefined);
262
+ if (token && token.expires > Date.now()) return token.access;
263
+ }
264
+
265
+ const request = requestAccessToken(clientId, clientSecret, signal);
266
+ tokenCache.set(key, request);
267
+ try {
268
+ return (await request).access;
269
+ } catch (error) {
270
+ tokenCache.delete(key);
271
+ throw error;
272
+ }
273
+ }
274
+
275
+ /** Model ids offered by one subscription. Doubles as the subscription check during login. */
276
+ async function fetchModelIds(subscription: string, token: string, signal?: AbortSignal): Promise<string[]> {
277
+ const response = await fetch(`${baseUrlFor(subscription)}/models`, {
278
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
279
+ signal,
280
+ });
281
+
282
+ if (!response.ok) {
283
+ throw new Error(
284
+ `${PROVIDER_NAME} model listing for subscription "${subscription}" failed (${response.status}): ${await errorDetail(response)}`,
285
+ );
286
+ }
287
+
288
+ const payload = (await response.json()) as { data?: { id?: string }[] };
289
+ return (payload.data ?? []).map((entry) => entry?.id).filter((id): id is string => typeof id === "string" && id.length > 0);
290
+ }
291
+
292
+ // =============================================================================
293
+ // Auth
294
+ // =============================================================================
295
+
296
+ /** Everything needed to reach one subscription: endpoint plus a way to get a token. */
297
+ interface Settings {
298
+ subscription: string;
299
+ token?: string;
300
+ clientId?: string;
301
+ clientSecret?: string;
302
+ source: string;
303
+ }
304
+
305
+ type EnvLookup = (name: string) => Promise<string | undefined> | string | undefined;
306
+
307
+ /**
308
+ * Collect settings from the stored credential first, then the environment.
309
+ * The subscription name is mandatory: without it there is no endpoint to call.
310
+ */
311
+ async function lookupSettings(env: EnvLookup, credential?: ApiKeyCredential): Promise<Settings | undefined> {
312
+ const stored = credential?.env ?? {};
313
+ const subscription = stored[ENV_SUBSCRIPTION] ?? (await env(ENV_SUBSCRIPTION));
314
+
315
+ const token = credential?.key ?? (await env(ENV_ACCESS_TOKEN));
316
+ const clientId = stored[ENV_CLIENT_ID] ?? (await env(ENV_CLIENT_ID));
317
+ const clientSecret = stored[ENV_CLIENT_SECRET] ?? (await env(ENV_CLIENT_SECRET));
318
+
319
+ const origin = token
320
+ ? credential?.key
321
+ ? "stored access token"
322
+ : ENV_ACCESS_TOKEN
323
+ : clientId && clientSecret
324
+ ? stored[ENV_CLIENT_ID]
325
+ ? "stored client credentials"
326
+ : `${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET}`
327
+ : undefined;
328
+ if (!origin) return undefined;
329
+
330
+ if (!subscription) {
331
+ throw new Error(
332
+ `No ${PROVIDER_NAME} subscription name — run /login ${PROVIDER_ID} or set ${ENV_SUBSCRIPTION}`,
333
+ );
334
+ }
335
+
336
+ return { subscription, token, clientId, clientSecret, source: `${origin}, subscription "${subscription}"` };
337
+ }
338
+
339
+ async function accessTokenFor(settings: Settings, signal?: AbortSignal): Promise<string> {
340
+ return settings.token ?? cachedAccessToken(settings.clientId!, settings.clientSecret!, signal);
341
+ }
342
+
343
+ const apiKeyAuth: ApiKeyAuth = {
344
+ name: `${PROVIDER_NAME} subscription and client credentials`,
345
+
346
+ /** Interactive setup started by /login: subscription name, Client ID, Client Secret. */
347
+ async login(interaction: AuthInteraction): Promise<ApiKeyCredential> {
348
+ const subscription = (
349
+ await interaction.prompt({
350
+ type: "text",
351
+ message: "Subscription name (part of the API base URL, no default)",
352
+ placeholder: "all-models",
353
+ })
354
+ ).trim();
355
+ if (!subscription) throw new Error("A subscription name is required");
356
+
357
+ const clientId = (
358
+ await interaction.prompt({ type: "text", message: "Client ID", placeholder: "OAuth 2.0 client id" })
359
+ ).trim();
360
+ const clientSecret = (await interaction.prompt({ type: "secret", message: "Client Secret" })).trim();
361
+ if (!clientId || !clientSecret) throw new Error("Client ID and Client Secret are required");
362
+
363
+ // Verify credentials and subscription before storing; also primes the token cache.
364
+ interaction.notify({ type: "progress", message: "Verifying client credentials..." });
365
+ const token = await cachedAccessToken(clientId, clientSecret, interaction.signal);
366
+
367
+ interaction.notify({ type: "progress", message: `Checking subscription "${subscription}"...` });
368
+ const modelIds = (await fetchModelIds(subscription, token, interaction.signal)).filter((id) => !isHidden(id));
369
+ interaction.notify({ type: "info", message: `${modelIds.length} usable model(s) in "${subscription}"` });
370
+
371
+ return {
372
+ type: "api_key",
373
+ env: {
374
+ [ENV_SUBSCRIPTION]: subscription,
375
+ [ENV_CLIENT_ID]: clientId,
376
+ [ENV_CLIENT_SECRET]: clientSecret,
377
+ },
378
+ };
379
+ },
380
+
381
+ async check({ ctx, credential }) {
382
+ const settings = await lookupSettings((name) => ctx.env(name), credential).catch(() => undefined);
383
+ return settings ? { type: "api_key", source: settings.source } : undefined;
384
+ },
385
+
386
+ async resolve({ ctx, credential }) {
387
+ const settings = await lookupSettings((name) => ctx.env(name), credential);
388
+ if (!settings) return undefined;
389
+
390
+ return {
391
+ auth: { apiKey: await accessTokenFor(settings), baseUrl: baseUrlFor(settings.subscription) },
392
+ env: { [ENV_SUBSCRIPTION]: settings.subscription },
393
+ source: settings.source,
394
+ };
395
+ },
396
+ };
397
+
398
+ // =============================================================================
399
+ // Model catalog
400
+ // =============================================================================
401
+
402
+ function toModel(id: string, baseUrl: string): SwissModel {
403
+ const meta = MODEL_METADATA[id] ?? {};
404
+ return {
405
+ id,
406
+ name: meta.name ?? id,
407
+ api: meta.api ?? DEFAULT_API,
408
+ provider: PROVIDER_ID,
409
+ baseUrl,
410
+ reasoning: meta.reasoning ?? DEFAULT_METADATA.reasoning,
411
+ ...(meta.thinkingLevelMap ? { thinkingLevelMap: meta.thinkingLevelMap } : {}),
412
+ input: meta.input ?? DEFAULT_METADATA.input,
413
+ cost: meta.cost ?? DEFAULT_METADATA.cost,
414
+ contextWindow: meta.contextWindow ?? DEFAULT_METADATA.contextWindow,
415
+ maxTokens: meta.maxTokens ?? DEFAULT_METADATA.maxTokens,
416
+ ...(meta.compat ? { compat: meta.compat } : {}),
417
+ };
418
+ }
419
+
420
+ /** Throws on failure so pi keeps the previously stored catalog. */
421
+ async function fetchModels({ credential, signal }: RefreshModelsContext): Promise<readonly SwissModel[]> {
422
+ const apiKeyCredential: ApiKeyCredential | undefined = credential?.type === "api_key" ? credential : undefined;
423
+ const settings = await lookupSettings((name) => process.env[name], apiKeyCredential);
424
+ if (!settings) {
425
+ throw new Error(`No ${PROVIDER_NAME} credentials — run /login ${PROVIDER_ID} or set ${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET}`);
426
+ }
427
+
428
+ const token = await accessTokenFor(settings, signal);
429
+ const baseUrl = baseUrlFor(settings.subscription);
430
+
431
+ return (await fetchModelIds(settings.subscription, token, signal))
432
+ .filter((id) => !isHidden(id))
433
+ .map((id) => toModel(id, baseUrl))
434
+ .sort((a, b) => a.name.localeCompare(b.name));
435
+ }
436
+
437
+ // =============================================================================
438
+ // Extension entry point
439
+ // =============================================================================
440
+
441
+ export default function (pi: ExtensionAPI) {
442
+ pi.registerProvider(
443
+ createProvider<SwissApi>({
444
+ id: PROVIDER_ID,
445
+ name: PROVIDER_NAME,
446
+ // Per-credential endpoint; the subscription-specific URL comes from auth.
447
+ baseUrl: PRODUCT_URL,
448
+ auth: { apiKey: apiKeyAuth },
449
+ models: [],
450
+ fetchModels,
451
+ // Second line of defence: also hides models restored from a stale catalog cache.
452
+ filterModels: (models) => models.filter((model) => !isHidden(model.id)),
453
+ // Per-model dispatch: completions for everything unless MODEL_METADATA says otherwise.
454
+ api: {
455
+ "openai-completions": openAICompletionsApi(),
456
+ "openai-responses": openAIResponsesApi(),
457
+ },
458
+ }),
459
+ );
460
+ }
package/package.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "pi-provider-swiss-ai-platform",
3
+ "version": "0.1.0",
4
+ "keywords": ["pi-package"],
5
+ "pi": {
6
+ "extensions": ["./index.ts"]
7
+ }
8
+ }