pi-sap-aicore 0.3.6 → 0.3.7

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,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.7] - 2026-07-16
11
+
12
+ ### Fixed
13
+
14
+ - Restored shared `/login` credentials for `sap-aicore-foundation` and
15
+ `/sap-models discover` on Pi 0.80.9+. Pi replaced the old synchronous
16
+ `AuthStorage.list()` / `get()` API; the extension now uses the public
17
+ `readStoredCredential()` helper and reads only the `sap-aicore` credential.
18
+ Previously the compatibility error was swallowed and every model request
19
+ incorrectly reported that no service key was configured.
20
+
21
+ ### Added
22
+
23
+ - Added `scripts/test-auth-storage.mjs`, an isolated regression test that checks
24
+ shared credential lookup, literal `$` preservation, provider isolation, and
25
+ credential-type validation.
26
+
27
+ ### Changed
28
+
29
+ - Raised the Pi peer and development dependency floor to 0.80.9, where the
30
+ supported synchronous stored-credential helper is available.
31
+
10
32
  ## [0.3.6] - 2026-07-04
11
33
 
12
34
  ### Fixed
@@ -221,7 +243,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
221
243
  `reasoning_effort` for OpenAI).
222
244
  - MIT license and npm packaging.
223
245
 
224
- [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.3...HEAD
246
+ [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.7...HEAD
247
+ [0.3.7]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.6...v0.3.7
248
+ [0.3.6]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.5...v0.3.6
249
+ [0.3.5]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.4...v0.3.5
250
+ [0.3.4]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.3...v0.3.4
225
251
  [0.3.3]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.2...v0.3.3
226
252
  [0.3.2]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.1...v0.3.2
227
253
  [0.3.1]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.0...v0.3.1
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.7",
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 {