原始内容
pi-sap-aicore
A custom provider extension for the pi coding agent that routes inference through SAP AI Core — via the orchestration service (every model from a single deployment) and/or direct foundation deployments (per-model foundation endpoints such as Azure OpenAI or AWS Bedrock). Both register at once and share one login, so you pick the route per model. See Orchestration vs. Foundation.
Prerequisites
- pi 0.81.0 or newer installed (
npm install -g @earendil-works/pi-coding-agent) - Node.js 22.19.0 or newer
- An SAP BTP account with AI Core entitlement and an orchestration deployment
- (optional, for the foundation provider) one or more foundation-models
deployments — one per model you want to route directly (
azure-openaifor GPT/OpenAI models,aws-bedrockfor Anthropic/Claude models) - The service key JSON for your AI Core service binding
[!IMPORTANT]
pi-sap-aicore0.4.0 and newer require pi 0.81.0 or newer because they use pi's complete native Provider API. If you must remain on pi 0.80.x, pin the previous compatible extension release until you can upgrade:pi install npm:pi-sap-aicore@0.3.8
Credentials
The extension looks for the SAP BTP service-key JSON in this order:
- Pi's auth store —
~/.pi/agent/auth.json, populated by/login(see below). Persisted across sessions, file-permission-locked by pi. AICORE_SERVICE_KEYenvironment variable — per-shell override. Useful for testing against a different tenant for one session without re-running/login.
If neither is present, inference fails with a clear "no service key configured" error.
Both providers — sap-aicore (orchestration) and sap-aicore-foundation — use the
same service key, so a single /login (or one AICORE_SERVICE_KEY) covers
both. pi keys stored credentials per provider, so the foundation provider reads the
shared login from pi's auth store directly; you never log in twice.
Recommended: /login
From inside pi:
/login
Then:
- Pick Sign in with an API key.
- Pick SAP AI Core.
- Paste your BTP service-key JSON as a single line and hit enter.
It's validated immediately — if anything is missing or malformed, you'll get a
specific error pointing at the field, so you can re-run
/loginand fix it.
Pi 0.81 stores the JSON as a native provider credential in
~/.pi/agent/auth.json. The value is passed to the provider verbatim, so literal
$ characters in clientsecret are preserved.
To get the JSON: BTP cockpit → your AI Core service instance → Service Keys → View. Copy the entire JSON object.
Upgrading from 0.3.x? Existing credentials created through Use a subscription → SAP AI Core continue to work without re-login. The Pi 0.81 provider retains that legacy credential handler solely for migration; new logins should use the native API-key flow above. The foundation provider still borrows the primary
sap-aicorecredential and does not add a second subscription entry. Pi may list SAP AI Core (Foundation) in the API-key provider picker because every complete Provider declares auth; choose the primary SAP AI Core entry when you want one credential shared by both.
Alternative: AICORE_SERVICE_KEY env var
export AICORE_SERVICE_KEY='{"clientid":"...","clientsecret":"...","url":"https://...authentication.sap.hana.ondemand.com","serviceurls":{"AI_API_URL":"https://api.ai.<region>.ml.hana.ondemand.com"}}'
The @sap-ai-sdk/orchestration SDK reads this directly for XSUAA auth, token
caching, and deployment resolution — no manual token plumbing needed.
Install
From npm (recommended)
pi install npm:pi-sap-aicore
pi downloads the package under ~/.pi/agent/npm/, runs npm install to pull the
SAP AI SDKs, and auto-loads the extension on every startup. Run the one command on
each machine; pi update keeps it current. Pin a version with
pi install npm:pi-sap-aicore@<version> (pinned specs are skipped by pi update).
Then configure credentials with /login (see Credentials) and
confirm the models are visible:
pi --list-models | grep sap-aicore
Local development (this repo)
npm install
pi -e ./index.ts --list-models
You'll see the orchestration models under sap-aicore/ (Claude, GPT-5*, Gemini),
plus any direct foundation models under sap-aicore-foundation/:
sap-aicore/anthropic--claude-4.7-opus— Claude Opus 4.7 (orchestration)sap-aicore/gpt-5.5— GPT-5.5 via orchestrationsap-aicore-foundation/gpt-5.5— GPT-5.5 via its direct Azure OpenAI foundation deploymentsap-aicore-foundation/anthropic--claude-4.8-opus— Claude Opus 4.8 via its direct AWS Bedrock foundation deployment
Run pi -e ./index.ts to launch pi with the local extension loaded; this
overrides any globally-installed version for the session, which is the fastest
iteration loop while developing.
Alternative: install from git
For an unpublished fork or a branch you want to track directly:
pi install git:github.com/ttiimmaahh/pi-sap-aicore@main
pi clones to ~/.pi/agent/git/…, runs npm install, and auto-loads on startup.
Note: an @main git install is not moved to newer commits by pi update (it
only reconciles to the pinned ref) — prefer the npm install above for hands-off
updates.
Orchestration vs. Foundation
The extension registers two providers, both backed by the same service key:
sap-aicore (orchestration) |
sap-aicore-foundation (direct) |
|
|---|---|---|
| SAP deployment | one orchestration deployment fronts every model | one foundation deployment per model |
| Models | Claude, GPT-5*, Gemini | GPT/OpenAI (azure-openai), Anthropic/Claude (aws-bedrock), and Gemini (gcp-vertexai) |
| Tool use | yes | yes — tools are translated to OpenAI, Bedrock Converse, or Vertex/Gemini function declarations by executable |
| Streaming | subject to orchestration's per-model allow-list — new models can 400 Streaming is not supported (we fall back to non-streaming) |
Azure OpenAI streams natively; AWS Bedrock and Vertex AI currently use non-streaming endpoints and replay responses into pi stream events |
| Reasoning effort | tunable (reasoning_effort / thinking) |
model default only for Azure; Bedrock/Anthropic and Vertex/Gemini effort controls are intentionally conservative |
| Content filter / grounding / templating | yes | no — raw model access |
| SDK / endpoint | @sap-ai-sdk/orchestration |
AzureOpenAiChatClient for azure-openai; SAP /inference/deployments/{id}/converse for aws-bedrock; SAP /inference/deployments/{id}/models/{model}:generateContent for gcp-vertexai |
Both routes appear in the model list simultaneously, so you choose per model. The
foundation route exists mainly to access new models directly when orchestration
lags behind model deployment or streaming support (for example gpt-5.5 on
Azure OpenAI or a newly deployed Claude model on AWS Bedrock).
Adding a foundation model: it needs its own foundation-models deployment in
SAP AI Core — one per (model, version, resource group). The extension chooses the
foundation executable from the model id: gpt-* → azure-openai,
anthropic--* → aws-bedrock, and gemini-* → gcp-vertexai. Then add its id to the per-machine extension overlay at
~/.pi/agent/pi-sap-aicore/models.json:
{
"foundation": { "enabledModelIds": ["gpt-5.5", "anthropic--claude-4.8-opus"] }
}
Definitions are reused from the shared catalog, so an id only has to be present
there. An id with no matching deployment 404s at call time. Run
/sap-models discover in pi (or node scripts/list-sap-models.mjs from this
repo) to see what your tenant actually deploys.
Live foundation validation
To validate normal coding-agent behavior across the three direct foundation executables, run the live smoke-test matrix from this repo:
npm run validate:foundation
The script uses pi --no-extensions -e ./index.ts so it tests the local checkout,
not a globally installed package. It makes real SAP model calls and validates:
- text generation
- real tool execution via side-effect files
- image input smoke tests
Default models are gpt-5.5, anthropic--claude-4.8-opus, and
gemini-3.5-flash. Override them with GPT_MODEL, BEDROCK_MODEL, and
VERTEX_MODEL; set SKIP_IMAGE=1 to skip vision tests.
Models
The model list is composed of three sources, merged at startup:
src/models-snapshot.json— packaged fallback catalog, auto-generated from models.dev's SAP AI Core catalog. Maintainers refresh it with:npm run update-modelsThis re-fetches the live catalog, applies our family-specific filters (currently anthropic claude-4.x, gpt-5*, gemini-2.5*), and writes the snapshot to disk. Commit the result.
~/.pi/agent/pi-sap-aicore/models-cache.json— per-machine public catalog cache. Pi 0.81 refreshes authenticated providers in the background when/modelopens and throughpi update --models; checks are throttled for four hours. Force an immediate refresh inside pi with:/sap-models updateThis does not edit the installed npm package and is safe across extension updates. The existing Provider objects publish the refreshed model list immediately, without re-registration or
/reload. A cache older than the bundled snapshot is ignored after extension upgrades.~/.pi/agent/pi-sap-aicore/models.json— per-machine tenant overlay. Use it for models in your tenant that are not in the public catalog yet, model overrides, exclusions, and foundation-route enablement. Overlay models win over cache/snapshot on duplicateid.
Example overlay:
{
"models": [
{
"id": "some-preview-model",
"name": "Some Preview Model",
"reasoning": true,
"tool_call": true,
"temperature": true,
"modalities": { "input": ["text"], "output": ["text"] },
"limit": { "context": 200000, "output": 32000 },
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"thinkingLevelMap": {
"minimal": "low",
"low": "low",
"medium": "medium",
"high": "high",
"xhigh": "high"
}
}
],
"overrides": {
"gemini-2.5-pro": { "reasoning": false }
},
"exclude": ["gpt-5.5"],
"foundation": {
"enabledModelIds": ["some-preview-model"]
}
}
Use /sap-models paths to print the exact cache and overlay paths, and
/sap-models discover to compare the loaded catalog against the models your SAP
tenant reports.
/sap-models commands
Run these inside pi after installing/loading the extension:
| Command | What it does |
|---|---|
/sap-models update |
Forces the latest public SAP AI Core model metadata from models.dev into ~/.pi/agent/pi-sap-aicore/models-cache.json and updates both live Provider views. |
/sap-models discover |
Uses your configured SAP service key to query the tenant's foundation-models scenario, then reports models that are missing from the local catalog and catalog entries absent from the tenant. Honors AICORE_RESOURCE_GROUP / service-key resourceGroup. |
/sap-models list |
Shows how many orchestration models and foundation-enabled models are currently loaded after snapshot/cache/overlay merging. |
/sap-models paths |
Prints the cache and overlay file paths for this machine. |
/sap-models help |
Shows the command summary in pi. |
A typical refresh workflow is:
/sap-models update
/sap-models discover
/model
If discover reports a tenant model that is missing from the catalog, add it to
~/.pi/agent/pi-sap-aicore/models.json under models. If it reports a catalog
model that is absent from your tenant and selection causes SAP 400s, add the id
to exclude.
Overlay reference
~/.pi/agent/pi-sap-aicore/models.json supports these top-level fields:
| Field | Type | Purpose |
|---|---|---|
models |
SapModel[] |
Adds tenant-only/pre-release models or replaces catalog models with the same id. |
overrides |
object keyed by model id | Partially overrides an existing model. Nested limit, cost, modalities, and thinkingLevelMap fields are merged. Unknown ids are ignored. |
exclude |
string[] |
Removes model ids after snapshot/cache/overlay merging. Useful for public catalog entries your SAP tenant does not deploy. |
foundation.enabledModelIds |
string[] |
Also exposes matching model ids through sap-aicore-foundation/*. Each id must exist in the merged catalog and have a foundation deployment in the selected resource group. |
Minimal tenant-only model:
{
"models": [
{
"id": "gpt-5.4-nano",
"name": "GPT-5.4 Nano",
"reasoning": true,
"tool_call": true,
"temperature": true,
"modalities": { "input": ["text", "image"], "output": ["text"] },
"limit": { "context": 1050000, "output": 128000 },
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"thinkingLevelMap": {
"minimal": "low",
"low": "low",
"medium": "medium",
"high": "high",
"xhigh": "high"
}
}
]
}
Minimal foundation enablement for a model already in the catalog:
{
"foundation": { "enabledModelIds": ["gpt-5.5"] }
}
The cost fields are vendor list prices (USD per million tokens) from
models.dev. Used only for pi's in-UI cost display — your actual SAP
BTP invoice is contract-based and will differ.
Thinking levels
Models with reasoning: true honor pi's thinking-level cycle (default
keybind Shift+Tab): off, minimal, low, medium, high, xhigh.
- Anthropic 4.6+ models (
anthropic--claude-4.6-*,4.7-*) use adaptive thinking —thinking: {type: "adaptive"}+output_config: {effort}. The model decides the budget; the level only nudges depth. - Older Anthropic models (
anthropic--claude-4-*,4.5-*) use budget-token thinking —thinking: {type: "enabled", budget_tokens: N}. Each pi level maps to a token count (1k / 4k / 8k / 16k / 32k for minimal/low/medium/high/xhigh), clamped down somax_tokensalways has at least 1024 tokens of headroom for the response. SAP rejects the adaptive shape on these models ("adaptive thinking is not supported on this model"), which is why we split.
Note on reasoning visibility: SAP orchestration does NOT pass
structured reasoning/thinking content through to streaming clients.
The model genuinely reasons (you'll see step-by-step structure leak
into the visible answer text, and the tokens are billed via
completion_tokens_details.reasoning_tokens), but pi's dedicated
"thinking" panel will stay empty for SAP-routed models — there's no
client-side fix. If SAP exposes a server-side flag for this in the
future, our pickReasoning probe is wired and ready in stream.ts.
- OpenAI models (
gpt-*) usereasoning_effort: "minimal" | "low" | "medium" | "high".xhighis omitted — OpenAI has no equivalent tier; pi will skip it when cycling. - Gemini models (
gemini-2.5-*) ship withreasoning: false— SAP's gemini reasoning passthrough is undocumented, so we keepShift+Taboff the cycle for these models rather than send a request shape SAP may reject. Wire-up (likelythinking_config.thinking_budget) is a future TODO insrc/stream.ts:reasoningParams.
Foundation route caveat: on sap-aicore-foundation/*, GPT/OpenAI models use
the direct Azure OpenAI SDK pinned to API version 2024-10-21, which has no
reasoning_effort field — so gpt-5* reason at their default effort and pi's
thinking-level cycle is a no-op there. Anthropic/Claude models use SAP's AWS
Bedrock /converse endpoint; Gemini models use SAP's Vertex AI generateContent
endpoint with thinkingBudget: 0 by default so small pi output budgets produce
visible text instead of only hidden thoughts. Tool/function calling is supported
on all three direct foundation executables. Use the orchestration route if you
need explicit effort control.
To override budgets per model, edit thinkingLevelMap on the relevant entry in
~/.pi/agent/pi-sap-aicore/models.json.
AI Resource Group
Resolved in this order:
AICORE_RESOURCE_GROUPenv var — per-shell override. Example:export AICORE_RESOURCE_GROUP=my-team-rgresourceGroupfield on the service-key JSON — convenient for teams who manage multiple groups and want to bake the default into the key. Non-standard, so add it yourself before pasting into pi:{ "clientid": "...", "clientsecret": "...", "resourceGroup": "my-team-rg", ... }SAP's server-side default (
default) — if neither of the above is set.
The value is passed via SAP's OrchestrationClient(..., {resourceGroup})
constructor arg, which is the only supported channel — AI-Resource-Group
as a request header is explicitly rejected by SAP's orchestration typings. The
foundation provider applies the same resolved group when resolving and invoking
direct deployments; both a model's foundation deployment and the orchestration
deployment must live in the resolved group for name-based resolution to find them.
Prompt caching & cost reporting
Cache read/write tokens always report 0 on SAP-routed turns. SAP
orchestration strips all detail fields from the TokenUsage response
— we only get prompt_tokens, completion_tokens, and total_tokens
across every route. There's no prompt_tokens_details.cached_tokens
(OpenAI) and no top-level cache_read_input_tokens (Anthropic) for
the client to read.
Whether the backend actually caches is invisible to pi. SAP's contract billing may give you a discount on cached tokens that this extension can't surface — check your BTP invoice if cache savings matter.
Experimental: PI_SAP_AICORE_CACHE_CONTROL=1 tags the system
prompt and last user message with Anthropic's cache_control: {type:"ephemeral"}. SAP may forward it (saving SAP money on the
backend, possibly passed through via your contract) or may 400 the
request. Either way, you won't see cacheRead become non-zero in pi's
diagnostics — that requires SAP to expose detail fields, which they
currently don't.
OpenAI/Gemini routes ignore the flag — they have their own automatic caching with no breakpoint API.
Foundation route: because direct foundation endpoints bypass orchestration's
usage-stripping, provider-specific cache fields may come back populated.
mapUsage reads OpenAI prompt_tokens_details.cached_tokens and Anthropic-style
cache-read fields when SAP exposes them, so cacheRead could be non-zero on
sap-aicore-foundation/* turns where orchestration always reports 0. Treat as
best-effort and provider-dependent.
Releasing (maintainers)
Releases are tag-driven and published to npm by GitHub Actions. There is no
build step — pi loads the .ts sources directly via jiti — so a release is just
verify + publish.
Update
CHANGELOG.md: move items from[Unreleased]into a new version heading.Bump the version (this commits
package.jsonand creates avX.Y.Ztag):npm version patch # or minor / major git push --follow-tagsThe
Publishworkflow fires on thev*tag, asserts the tag matchespackage.json, typechecks, publishes to npm, and creates/updates the matching GitHub Release from that version'sCHANGELOG.mdnotes.
Every push to main and every PR also runs the CI
typecheck gate.
One-time setup: npm Trusted Publishing (OIDC)
Publishing is tokenless — no NPM_TOKEN secret. Authorize this repo once on
npmjs.com:
- npmjs.com → the
pi-sap-aicorepackage → Settings → Trusted Publisher. - Choose GitHub Actions and enter (case-sensitive, exact match):
- Organization or user:
ttiimmaahh - Repository:
pi-sap-aicore - Workflow filename:
publish.yml - Allowed actions:
npm publish
- Organization or user:
- Save. The next
v*tag publishes automatically, with provenance attestations.
The first CI release must be a version newer than the last manually published one (
0.1.0) — npm rejects republishing an existing version.
Repo layout
.
├── package.json # pi-package manifest + deps + scripts
├── tsconfig.json # editor support; pi runs the .ts directly
├── CHANGELOG.md # Keep a Changelog; updated per release
├── LICENSE # MIT
├── .github/workflows/
│ ├── ci.yml # typecheck gate on push to main + PRs
│ └── publish.yml # tag-driven npm publish via OIDC trusted publishing
├── index.ts # ExtensionAPI factory + complete Provider registration
├── scripts/
│ ├── update-models.mjs # maintainer script: fetches models.dev, writes models-snapshot.json
│ ├── list-sap-models.mjs # lists models your tenant actually deploys (diff vs snapshot)
│ ├── diagnose-streaming.mjs # probes orchestration streaming support per model
│ ├── test-native-providers.mjs # offline Provider/auth compatibility checks
│ ├── test-model-refresh.mjs # offline refresh/cache/failure checks
│ └── validate-foundation-executables.mjs # live text/tool/image smoke tests for direct foundation executables
└── src/
├── auth.ts # native API-key auth + legacy credential compatibility
├── model-catalog.ts # loads snapshot/cache/overlay and adapts models.dev metadata
├── model-catalog-controller.ts # shared synchronous state + refresh lifecycle
├── models-snapshot.json # auto-generated from models.dev (committed)
├── providers.ts # complete Pi 0.81 Provider objects
├── sap-model-commands.ts # /sap-models update/discover/list/paths
├── to-pi-model.ts # SapModel → pi-ai Model mapper
├── stream.ts # orchestration stream adapter + shared usage/error helpers
├── translate.ts # pi Context ↔ orchestration message shape
├── foundation-executables.ts # model id → SAP foundation executable mapping
├── foundation-deployment.ts # shared foundation deployment resolution helpers
├── foundation-params.ts # Azure OpenAI request params (max_completion_tokens, temperature gating)
├── stream-foundation.ts # foundation dispatcher
├── stream-foundation-azure-openai.ts # AzureOpenAiChatClient adapter with native streaming
├── stream-foundation-bedrock.ts # AWS Bedrock /converse adapter for Anthropic foundation deployments
├── stream-foundation-vertexai.ts # Vertex AI generateContent adapter for Gemini foundation deployments
├── translate-foundation.ts # pi Context ↔ Azure OpenAI message shape
├── translate-foundation-bedrock.ts # pi Context ↔ Bedrock Converse message shape
└── translate-foundation-vertexai.ts # pi Context ↔ Vertex AI generateContent message shape