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 +37 -1
- package/index.ts +10 -9
- package/package.json +7 -7
- package/scripts/test-auth-storage.mjs +95 -0
- package/src/sap-model-commands.ts +2 -26
- package/src/stream.ts +17 -22
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.
|
|
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
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
49
|
-
"@earendil-works/pi-coding-agent": "^0.80.
|
|
50
|
-
"typescript": "^
|
|
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
|
-
|
|
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 {
|
|
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
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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 {
|