pi-sap-aicore 0.3.6 → 0.3.8

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/CHANGELOG.md CHANGED
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.8] - 2026-07-18
11
+
12
+ ### Fixed
13
+
14
+ - SAP AI Core no longer appears configured in `/login` on a fresh install.
15
+ The orchestration provider now declares its `AICORE_SERVICE_KEY` environment
16
+ fallback explicitly instead of using an always-configured placeholder; stored
17
+ subscription credentials continue to use the existing OAuth-backed login.
18
+
19
+ ## [0.3.7] - 2026-07-16
20
+
21
+ ### Fixed
22
+
23
+ - Restored shared `/login` credentials for `sap-aicore-foundation` and
24
+ `/sap-models discover` on Pi 0.80.9+. Pi replaced the old synchronous
25
+ `AuthStorage.list()` / `get()` API; the extension now uses the public
26
+ `readStoredCredential()` helper and reads only the `sap-aicore` credential.
27
+ Previously the compatibility error was swallowed and every model request
28
+ incorrectly reported that no service key was configured.
29
+
30
+ ### Added
31
+
32
+ - Added `scripts/test-auth-storage.mjs`, an isolated regression test that checks
33
+ shared credential lookup, literal `$` preservation, provider isolation, and
34
+ credential-type validation.
35
+
36
+ ### Changed
37
+
38
+ - Raised the Pi peer and development dependency floor to 0.80.9, where the
39
+ supported synchronous stored-credential helper is available.
40
+
10
41
  ## [0.3.6] - 2026-07-04
11
42
 
12
43
  ### Fixed
@@ -221,7 +252,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
221
252
  `reasoning_effort` for OpenAI).
222
253
  - MIT license and npm packaging.
223
254
 
224
- [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.3...HEAD
255
+ [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.8...HEAD
256
+ [0.3.8]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.7...v0.3.8
257
+ [0.3.7]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.6...v0.3.7
258
+ [0.3.6]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.5...v0.3.6
259
+ [0.3.5]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.4...v0.3.5
260
+ [0.3.4]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.3...v0.3.4
225
261
  [0.3.3]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.2...v0.3.3
226
262
  [0.3.2]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.1...v0.3.2
227
263
  [0.3.1]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.0...v0.3.1
package/index.ts CHANGED
@@ -17,13 +17,11 @@ const PROVIDER_API = "sap-aicore-orchestration" as Api;
17
17
  const FOUNDATION_PROVIDER_NAME = "sap-aicore-foundation";
18
18
  const FOUNDATION_PROVIDER_API = "sap-aicore-foundation" as Api;
19
19
 
20
- // pi requires a non-empty `apiKey` for any custom provider that defines models
21
- // (model-registry `validateConfig`), even when credentials come from `oauth`.
22
- // This value is never used: the real key is supplied by `sapAiCoreOAuth`
23
- // (after `/login`) or by AICORE_SERVICE_KEY (both handled in stream.ts). It is a
24
- // plain lowercase literal so pi's config-value resolver returns it as-is — no
25
- // `$` interpolation, no shell exec, and not mistaken for a legacy env-var name.
26
- const PLACEHOLDER_API_KEY = "managed-by-extension-oauth";
20
+ // The foundation provider has no OAuth entry of its own: it shares the
21
+ // orchestration provider's stored login in stream.ts. Pi still requires every
22
+ // provider to expose an authentication method, so this keeps foundation models
23
+ // selectable without adding a duplicate SAP AI Core entry to `/login`.
24
+ const FOUNDATION_PLACEHOLDER_API_KEY = "managed-by-extension-oauth";
27
25
 
28
26
  export default function (pi: ExtensionAPI) {
29
27
  const registerProviders = () => {
@@ -36,7 +34,10 @@ export default function (pi: ExtensionAPI) {
36
34
  pi.registerProvider(PROVIDER_NAME, {
37
35
  name: "SAP AI Core",
38
36
  baseUrl: "https://sap-aicore-handled-by-sdk.invalid",
39
- apiKey: PLACEHOLDER_API_KEY,
37
+ // Unlike a literal placeholder, this only makes Pi report the provider as
38
+ // configured when the environment variable is actually present. Stored
39
+ // subscription credentials continue to flow through `oauth` below.
40
+ apiKey: "$AICORE_SERVICE_KEY",
40
41
  api: PROVIDER_API,
41
42
  // Credentials flow through pi's `oauth` path — its escape hatch from the
42
43
  // $-interpolating config-value resolver that corrupts service keys
@@ -69,7 +70,7 @@ export default function (pi: ExtensionAPI) {
69
70
  pi.registerProvider(FOUNDATION_PROVIDER_NAME, {
70
71
  name: "SAP AI Core (Foundation)",
71
72
  baseUrl: "https://sap-aicore-handled-by-sdk.invalid",
72
- apiKey: PLACEHOLDER_API_KEY,
73
+ apiKey: FOUNDATION_PLACEHOLDER_API_KEY,
73
74
  api: FOUNDATION_PROVIDER_API,
74
75
  models: foundationModels.map((m) =>
75
76
  toPiModel(m, FOUNDATION_PROVIDER_API),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sap-aicore",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "SAP AI Core (orchestration + foundation) provider for the pi coding agent",
5
5
  "license": "MIT",
6
6
  "author": "Tim Pearson (https://github.com/ttiimmaahh)",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "scripts": {
33
33
  "update-models": "node scripts/update-models.mjs",
34
- "test": "node scripts/test-vertex-tool-schema.mjs && node scripts/test-bedrock-tool-results.mjs && node scripts/test-empty-content-blocks.mjs",
34
+ "test": "node scripts/test-auth-storage.mjs && node scripts/test-vertex-tool-schema.mjs && node scripts/test-bedrock-tool-results.mjs && node scripts/test-empty-content-blocks.mjs",
35
35
  "validate:foundation": "node scripts/validate-foundation-executables.mjs",
36
36
  "prepublishOnly": "tsc --noEmit && npm test"
37
37
  },
@@ -41,13 +41,13 @@
41
41
  "@sap-ai-sdk/orchestration": "^2.12.0"
42
42
  },
43
43
  "peerDependencies": {
44
- "@earendil-works/pi-ai": "*",
45
- "@earendil-works/pi-coding-agent": "*"
44
+ "@earendil-works/pi-ai": ">=0.80.9",
45
+ "@earendil-works/pi-coding-agent": ">=0.80.9"
46
46
  },
47
47
  "devDependencies": {
48
- "@earendil-works/pi-ai": "^0.80.3",
49
- "@earendil-works/pi-coding-agent": "^0.80.3",
50
- "typescript": "^6.0.3"
48
+ "@earendil-works/pi-ai": "^0.80.9",
49
+ "@earendil-works/pi-coding-agent": "^0.80.9",
50
+ "typescript": "^7.0.2"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=20"
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // Offline regression test for sharing the orchestration provider's stored
3
+ // subscription credential with the foundation provider. This makes no network
4
+ // calls and uses an isolated temporary auth file.
5
+
6
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+
11
+ const ROOT = new URL("..", import.meta.url).pathname;
12
+ const { readSharedServiceKeyFromStore } = await import(
13
+ pathToFileURL(join(ROOT, "src/stream.ts")).href
14
+ );
15
+
16
+ let failures = 0;
17
+ function check(condition, message) {
18
+ if (condition) {
19
+ console.log(` ✓ ${message}`);
20
+ return;
21
+ }
22
+ console.error(` ❌ ${message}`);
23
+ failures++;
24
+ }
25
+
26
+ const serviceKey = JSON.stringify({
27
+ clientid: "client-id",
28
+ clientsecret: "literal$secret",
29
+ url: "https://auth.example.test",
30
+ serviceurls: { AI_API_URL: "https://api.example.test" },
31
+ });
32
+ const tempDir = mkdtempSync(join(tmpdir(), "pi-sap-aicore-auth-"));
33
+ const authPath = join(tempDir, "auth.json");
34
+
35
+ try {
36
+ console.log("Shared OAuth credential lookup");
37
+ writeFileSync(
38
+ authPath,
39
+ JSON.stringify({
40
+ "sap-aicore": {
41
+ type: "oauth",
42
+ serviceKey,
43
+ access: "",
44
+ refresh: "",
45
+ expires: Number.MAX_SAFE_INTEGER,
46
+ },
47
+ }),
48
+ );
49
+ check(
50
+ readSharedServiceKeyFromStore(authPath) === serviceKey,
51
+ "reads the sap-aicore OAuth credential verbatim",
52
+ );
53
+ check(
54
+ readSharedServiceKeyFromStore(authPath)?.includes("literal$secret"),
55
+ "preserves literal dollar signs",
56
+ );
57
+
58
+ console.log("Provider isolation");
59
+ writeFileSync(
60
+ authPath,
61
+ JSON.stringify({
62
+ "another-provider": {
63
+ type: "oauth",
64
+ serviceKey,
65
+ access: "",
66
+ refresh: "",
67
+ expires: Number.MAX_SAFE_INTEGER,
68
+ },
69
+ }),
70
+ );
71
+ check(
72
+ readSharedServiceKeyFromStore(authPath) === undefined,
73
+ "does not borrow another provider's credential",
74
+ );
75
+
76
+ console.log("Credential type validation");
77
+ writeFileSync(
78
+ authPath,
79
+ JSON.stringify({
80
+ "sap-aicore": { type: "api_key", key: serviceKey },
81
+ }),
82
+ );
83
+ check(
84
+ readSharedServiceKeyFromStore(authPath) === undefined,
85
+ "ignores non-OAuth credentials",
86
+ );
87
+ } finally {
88
+ rmSync(tempDir, { recursive: true, force: true });
89
+ }
90
+
91
+ if (failures > 0) {
92
+ console.error(`\n${failures} check(s) failed`);
93
+ process.exit(1);
94
+ }
95
+ console.log("\nAll checks passed");
@@ -1,7 +1,4 @@
1
- import {
2
- AuthStorage,
3
- type ExtensionAPI,
4
- } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
2
  import { ScenarioApi } from "@sap-ai-sdk/ai-api";
6
3
 
7
4
  import { parseAndValidateServiceKey } from "./auth.ts";
@@ -14,31 +11,10 @@ import {
14
11
  } from "./model-catalog.ts";
15
12
  import { ensureServiceKey, resolveResourceGroup } from "./stream.ts";
16
13
 
17
- function sharedServiceKeyFromAuthStore(): string | undefined {
18
- try {
19
- const store = AuthStorage.create();
20
- for (const provider of store.list()) {
21
- const cred = store.get(provider);
22
- if (cred?.type !== "oauth") continue;
23
- const serviceKey = (cred as { serviceKey?: unknown }).serviceKey;
24
- if (
25
- typeof serviceKey === "string" &&
26
- serviceKey.trimStart().startsWith("{")
27
- ) {
28
- return serviceKey;
29
- }
30
- }
31
- } catch {
32
- // Let callers produce the actionable no-key message.
33
- }
34
- return undefined;
35
- }
36
-
37
14
  function resolveCommandServiceKey(): ReturnType<
38
15
  typeof parseAndValidateServiceKey
39
16
  > {
40
- const raw = process.env.AICORE_SERVICE_KEY ?? sharedServiceKeyFromAuthStore();
41
- return ensureServiceKey(raw);
17
+ return ensureServiceKey(undefined);
42
18
  }
43
19
 
44
20
  function formatModelList(ids: string[], max = 30): string {
package/src/stream.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  type SimpleStreamOptions,
15
15
  type Usage,
16
16
  } from "@earendil-works/pi-ai";
17
- import { AuthStorage } from "@earendil-works/pi-coding-agent";
17
+ import { readStoredCredential } from "@earendil-works/pi-coding-agent";
18
18
  import type { ChatModel, LlmModelParams } from "@sap-ai-sdk/orchestration";
19
19
  import type { TokenUsage } from "@sap-ai-sdk/orchestration/internal.js";
20
20
 
@@ -575,27 +575,22 @@ export function latchFinishReason(
575
575
 
576
576
  let lastValidatedKey: ValidatedKey | undefined;
577
577
 
578
- // pi stores oauth credentials keyed by PROVIDER name, not by the oauth `name`.
579
- // So a `/login` under `sap-aicore` is NOT automatically handed to a second
580
- // provider (`sap-aicore-foundation`) that shares the same oauth object — pi
581
- // passes that provider the registration placeholder instead, and we'd wrongly
582
- // report "no key configured". Recover the shared login by reading pi's own auth
583
- // store directly and returning the first sibling oauth credential that carries
584
- // a service-key JSON. This is what makes one `/login` serve both providers.
585
- function readSharedServiceKeyFromStore(): string | undefined {
586
- try {
587
- const store = AuthStorage.create();
588
- for (const provider of store.list()) {
589
- const cred = store.get(provider);
590
- if (cred?.type !== "oauth") continue;
591
- const sk = (cred as { serviceKey?: unknown }).serviceKey;
592
- if (typeof sk === "string" && sk.trimStart().startsWith("{")) return sk;
593
- }
594
- } catch {
595
- // Auth store unreadable (missing/locked/format change) — fall through to
596
- // the actionable "no key configured" error below.
597
- }
598
- return undefined;
578
+ // pi stores OAuth credentials by provider id, not by the OAuth display name.
579
+ // The foundation provider therefore reads the orchestration provider's stored
580
+ // login explicitly. `readStoredCredential` is the synchronous, public helper
581
+ // for stream setup; AuthStorage's old synchronous list/get API was removed in
582
+ // pi 0.80.9.
583
+ export function readSharedServiceKeyFromStore(
584
+ authPath?: string,
585
+ ): string | undefined {
586
+ const credential = readStoredCredential("sap-aicore", authPath);
587
+ if (credential?.type !== "oauth") return undefined;
588
+
589
+ const serviceKey = (credential as { serviceKey?: unknown }).serviceKey;
590
+ return typeof serviceKey === "string" &&
591
+ serviceKey.trimStart().startsWith("{")
592
+ ? serviceKey
593
+ : undefined;
599
594
  }
600
595
 
601
596
  export function ensureServiceKey(apiKey: string | undefined): ValidatedKey {