pi-bob 0.2.4 → 0.2.6
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/README.md +39 -11
- package/extensions/bob.ts +59 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,18 +4,19 @@ Pi provider package for IBM Bob / IBM-approved enterprise model endpoints.
|
|
|
4
4
|
|
|
5
5
|
The package registers Bob through Pi's built-in compatible provider APIs and discovers the currently exposed model catalog from Bob's authenticated `/inference/v1/model/info` endpoint. It does not scrape Bob, extract browser/session credentials, or bypass IBM-approved access paths.
|
|
6
6
|
|
|
7
|
-
> **✅ Works with IBM Bob Shell / Bob CLI 2.x (verified against `bobshell@2.0.
|
|
8
|
-
>
|
|
7
|
+
> **✅ Works with IBM Bob Shell / Bob CLI 2.x (verified against `bobshell@2.0.6`, 2026-10-11).**
|
|
8
|
+
> Routing headers, `model/info` payload shape, live discovery, and end-to-end inference were all re-checked on 2.0.6: the catalog now exposes 10 models — `premium`, `premium-ide`, `premium-shell`, `fast`, `explorer`, `wxO-model` (1M ctx), `background`, `security`, `gpt-oss-20b`, and `openai/gpt-oss-20b`. SSO login and token refresh were last exercised on 2.0.1; see [Validation performed](#validation-performed). One caveat: under 2.x, Bob only accepts the default **`openai-completions`** adapter; `anthropic-messages` and `openai-responses` are rejected with 403 by Bob-side entitlement. 1.x Bob Shell installations keep working — the parser accepts both payload shapes. See [Discovered local Bob Shell settings](#discovered-local-bob-shell-settings) for the full 2.x change list.
|
|
9
9
|
|
|
10
10
|
## Discovered local Bob Shell settings
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
First characterised on the installed `bobshell@2.0.1` package (upgraded from 1.0.6) and re-verified on `bobshell@2.0.6` against the live endpoint on 2026-10-11:
|
|
13
13
|
|
|
14
14
|
- Bob Shell 2.x moved stored SSO tokens to `~/.bob/settings/auth-secrets.json` (key `bob.auth.tokens-<gateway>`); `~/.bob/settings.json` keeps only non-secret settings. This extension still reads only `ibm.instanceId`/`ibm.teamId` from there and still ignores Bob's secrets.
|
|
15
15
|
- Token endpoints are unchanged in practice: 2.x builds `${gateway}/authn` + `v1/auth/token|refresh`, which resolves to the same `/authn/v1/auth/token` and `/authn/v1/auth/refresh` routes used here. The web login URL is now assembled as `https://bob.ibm.com` + `/login`; the final URL is the same as the 1.x constant.
|
|
16
|
-
- **
|
|
17
|
-
-
|
|
18
|
-
- Model tiers were renamed: 1.x `premium`/`pro`/`flash`/`flash-lite` became 2.x tiers `fast`/`premium`/`ultra
|
|
16
|
+
- **Payload shape:** 2.x `model/info` entries carry `max_output_tokens` and `supports_prompt_caching` alongside `max_tokens`; `litellm_params` (the backend identifier) was absent in 2.0.1 and is present again in 2.0.6. The parser accepts both shapes and falls back to the route name when the backend is missing. `max_output_tokens` is the enforced output cap under 2.x (verified 2026-08-31: `premium` accepted `max_tokens=50000` despite a catalog `max_tokens` of 12000) and is advertised when present.
|
|
17
|
+
- 2.x requires the `x-instance-id` header (HTTP 400 `X-Instance-ID header is required` without it). `x-team-id` is optional for `model/info` (HTTP 200 without it) but **required for inference** — a `chat/completions` request without it returns HTTP 402 `team user not found or has been deleted` (verified 2026-10-11). The extension always sends both, from Bob settings or the JWT `instances` claim.
|
|
18
|
+
- Model tiers were renamed: 1.x `premium`/`pro`/`flash`/`flash-lite` became 2.x tiers `fast`/`premium`/`ultra`. The 2.0.6 catalog is `premium`, `premium-ide`/`premium-shell` (270K ctx), `fast`, `explorer`, `wxO-model` (1M ctx / 64K out), and the utility routes `background`, `security`, `gpt-oss-20b`, `openai/gpt-oss-20b` (131,072 ctx) — all with `supports_vision` except the utility routes. The catalog changes server-side without a CLI release: `ultra`, `sonnet-4.5`, and the granite/rnj code models were exposed in 2.0.1 and are gone in 2.0.6, and `explorer` became visible. The `premium` alias exists in both, so the fallback catalog stays valid.
|
|
19
|
+
- No route in the 2.0.6 catalog publishes `supports_reasoning`, `supports_reasoning_effort`, or `supports_thinking`, so every Bob model is advertised as non-reasoning. `premium` does return `reasoning_content` when asked for `reasoning_effort`, so reasoning is opt-in via `IBM_BOB_REASONING_MODELS=premium` (with `IBM_BOB_SUPPORTS_REASONING_EFFORT=true`).
|
|
19
20
|
- **Breaking (entitlement):** under 2.0, the `anthropic-messages` and `openai-responses` routes return `403 Access denied` with a valid token (routes exist; access is denied for this instance). Only the default `openai-completions` adapter works; treat the alternates as unavailable until IBM grants access.
|
|
20
21
|
|
|
21
22
|
From the installed `bobshell@1.0.6` package and the local redacted Bob configuration (historical):
|
|
@@ -48,7 +49,7 @@ Set `IBM_BOB_API` to one of Pi's compatible API adapters:
|
|
|
48
49
|
- `openai-responses` — OpenAI Responses-compatible routes.
|
|
49
50
|
- `anthropic-messages` — Anthropic Messages-compatible routes.
|
|
50
51
|
|
|
51
|
-
Under Bob Shell 2.x, Bob currently accepts only the default `openai-completions` adapter; the other two return `403 Access denied` regardless of extension settings (verified 2026-
|
|
52
|
+
Under Bob Shell 2.x, Bob currently accepts only the default `openai-completions` adapter; the other two return `403 Access denied` regardless of extension settings (re-verified 2026-10-11).
|
|
52
53
|
|
|
53
54
|
The extension registers provider id `ibm-bob`. It uses an isolated `ibm-bob-compatible` Pi API adapter internally, then delegates serialization and streaming to the selected built-in adapter. This prevents Bob-specific authentication rules from affecting other providers.
|
|
54
55
|
|
|
@@ -61,7 +62,7 @@ Model discovery is enabled by default:
|
|
|
61
62
|
- Entries returned by the authenticated catalog are registered when `model_info.exposed` is omitted or `true`. Routes explicitly marked `exposed: false` are ignored.
|
|
62
63
|
- HTTP failures, timeouts, malformed responses, empty catalogs, and catalogs with no visible models retain the previous SSO catalog or fall back to `IBM_BOB_MODELS`. Bob still enforces route access during inference.
|
|
63
64
|
|
|
64
|
-
Discovered context limits, output limits, vision support, reasoning support, backend identifiers
|
|
65
|
+
Discovered context limits, output limits, vision support, reasoning support, and backend identifiers are mapped into Pi model definitions. Pricing is not discovered — Bob reports `0` for every per-token price — so costs come from the [Bobcoin rate table](#bobcoin-billing) instead.
|
|
65
66
|
|
|
66
67
|
### How context/output limits are determined
|
|
67
68
|
|
|
@@ -69,7 +70,7 @@ Every advertised number comes from Bob's own `/model/info` catalog — the exten
|
|
|
69
70
|
|
|
70
71
|
- **Context window** ← `model_info.max_input_tokens`.
|
|
71
72
|
- **Max output tokens** ← `model_info.max_output_tokens` when present (2.x), else `model_info.max_tokens` (the 1.x output cap).
|
|
72
|
-
- Models that report no limits (e.g. `granite-8b-code-instruct
|
|
73
|
+
- Models that report no limits (e.g. the 1.x `granite-8b-code-instruct` and `rnj-1-*` routes) use the defaults: 200,000 context / 8,192 output, overridable via `IBM_BOB_CONTEXT_WINDOW` / `IBM_BOB_MAX_TOKENS`.
|
|
73
74
|
|
|
74
75
|
The 2.x precedence was determined empirically, not from documentation. The 2.x catalog reports *two* output fields for the same route (e.g. `premium`: `max_tokens: 12000`, `max_output_tokens: 64000`), so on 2026-08-31 the extension author probed the live endpoint: a `chat/completions` request for `premium` with `max_tokens=50000` — four times the catalog's `max_tokens` — returned HTTP `200` and completed normally. That proves `max_tokens` is no longer the enforced cap and `max_output_tokens` is the real limit, so the larger, newer field is advertised when present. 1.x payloads have no `max_output_tokens`, where `max_tokens` was verified to be the output cap, keeping that behavior unchanged.
|
|
75
76
|
|
|
@@ -151,8 +152,26 @@ For SSO, do **not** copy a token out of Bob's local credential store unless IBM
|
|
|
151
152
|
| `IBM_BOB_INPUT` | discovered; fallback `text` | Override input types with `text` or `text,image`. |
|
|
152
153
|
| `IBM_BOB_REASONING` | discovered; fallback `false` | Explicitly enable or disable reasoning for all models. |
|
|
153
154
|
| `IBM_BOB_REASONING_MODELS` | empty | Comma-separated model IDs to mark as reasoning-capable. |
|
|
155
|
+
| `IBM_BOB_CREDITS_PER_MILLION` | per-route table below | Override the Bobcoin rate for every model, in Bobcoins per million tokens. |
|
|
154
156
|
|
|
155
|
-
|
|
157
|
+
Pricing comes from the Bobcoin rate table below, because Bob's model-info response reports `0` for every per-token price. If IBM ever populates those fields, the catalog values win and are converted from per-token to Pi's per-million-token units.
|
|
158
|
+
|
|
159
|
+
### Bobcoin billing
|
|
160
|
+
|
|
161
|
+
Bob bills a flat **Bobcoin** rate per route. Bobcoins are IBM's consumption meter at **$0.50 each**, drawn from a monthly plan allocation (Free trial 50, Pro 50, Pro+ 180, Ultra 1000) with opt-in pay-as-you-go overage at the same rate.
|
|
162
|
+
|
|
163
|
+
Measured against the live endpoint on 2026-10-11 (`bobshell@2.0.6`): `credits == rate × (prompt + completion) tokens / 1e6`, exact to four decimals from 254 to 9,780 input tokens, with **input and output priced identically and no discount for cache reads or writes** — a repeated 9,913-token prefix billed the same on the cache-write and cache-hit turns.
|
|
164
|
+
|
|
165
|
+
| Route | Bobcoins/M | Pi cost (at $0.50) |
|
|
166
|
+
| --- | --- | --- |
|
|
167
|
+
| `premium`, `premium-ide`, `premium-shell`, `wxO-model` | 2.0 | $1.00/M |
|
|
168
|
+
| `background`, `security` | 2.0 | $1.00/M |
|
|
169
|
+
| `fast`, `explorer` | 0.8333 | $0.4167/M |
|
|
170
|
+
| `gpt-oss-20b`, `openai/gpt-oss-20b` | 0 | free |
|
|
171
|
+
|
|
172
|
+
Any route not in the table is priced at the 2.0 rate. Because the rate is flat, Pi's cost total is exact whichever bucket Pi puts cached tokens in — verified end to end: a 21,941-token `premium` turn reported `$0.021941`, which is 0.043882 Bobcoins at $0.50.
|
|
173
|
+
|
|
174
|
+
The dollar figure is the **overage rate**, not marginal cost: while you are inside your plan allocation the Bobcoins are already paid for, so treat the footer as a burn gauge rather than a bill. Re-measure if the numbers look off — IBM can reprice without a CLI release.
|
|
156
175
|
|
|
157
176
|
### OpenAI compatibility toggles
|
|
158
177
|
|
|
@@ -169,9 +188,18 @@ It also rejects OpenAI-specific `store` and `prompt_cache_key` request propertie
|
|
|
169
188
|
|
|
170
189
|
## Validation performed
|
|
171
190
|
|
|
191
|
+
### Verified against Bob Shell 2.0.6 (2026-10-11)
|
|
192
|
+
|
|
193
|
+
- Live discovery through Pi: `pi -e . --list-models` shows all 10 current catalog models (`premium`, `premium-ide`, `premium-shell`, `fast`, `explorer`, `wxO-model`, `background`, `security`, `gpt-oss-20b`, `openai/gpt-oss-20b`) from any directory, using stored SSO credentials.
|
|
194
|
+
- End-to-end inference through `openai-completions`: `pi -e . --model ibm-bob/premium -p 'Reply with exactly: pi-bob-ok'` returned `pi-bob-ok`, and `--model ibm-bob/openai/gpt-oss-20b` returned `slash-id-ok`, so two-segment model IDs flow through selection and inference. A raw `POST /inference/v1/chat/completions` for `premium` returned `200` without the signing headers, so request signing is still unenforced.
|
|
195
|
+
- `anthropic-messages` and `openai-responses` routes: `403 Access denied` with a valid token — Bob-side entitlement, not an extension defect.
|
|
196
|
+
- `bun test`: 37 tests pass.
|
|
197
|
+
- When probing by hand, set `User-Agent: pi-bob/0.2.2` (what the extension sends). Requests from `curl`/`python-urllib` default UAs are rejected by Cloudflare in front of Bob with a `403` HTML page, which looks like an entitlement failure but is not.
|
|
198
|
+
- Not re-verified on 2.0.6: SSO token refresh (last confirmed on 2.0.1, 2026-08-31).
|
|
199
|
+
|
|
172
200
|
### Verified against Bob Shell 2.0.1 (2026-08-31)
|
|
173
201
|
|
|
174
|
-
- Live discovery through Pi: `pi --list-models`
|
|
202
|
+
- Live discovery through Pi: `pi --list-models` showed all 13 catalog models then exposed (`premium`, `premium-ide`, `premium-shell`, `ultra`, `fast`, `explorer`, `sonnet-4.5`, `wxO-model`, `gpt-oss-20b`, granite/rnj) from any directory, using stored SSO credentials.
|
|
175
203
|
- End-to-end inference through `openai-completions`: `premium`, `ultra`, and `premium-shell` all responded correctly to exact-reply smokes.
|
|
176
204
|
- SSO refresh on `/authn/v1/auth/refresh` returned `200` with a rotated refresh token; the refreshed token immediately passed `model/info`.
|
|
177
205
|
- `anthropic-messages` and `openai-responses` routes: `403 Access denied` with a valid token — Bob-side entitlement change, not an extension defect.
|
package/extensions/bob.ts
CHANGED
|
@@ -87,6 +87,27 @@ const CATALOG_CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/;
|
|
|
87
87
|
const CATALOG_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
88
88
|
const SUPPORTED_APIS = new Set<CompatibleApi>(["openai-completions", "openai-responses", "anthropic-messages"]);
|
|
89
89
|
|
|
90
|
+
// Bob bills a flat Bobcoin rate per route: credits == rate * (prompt + completion)
|
|
91
|
+
// tokens / 1e6, with no discount for cache reads or writes (measured 2026-10-11 on
|
|
92
|
+
// bobshell@2.0.6, exact to 4dp from 254 to 9,780 input tokens). Bobcoins are IBM's
|
|
93
|
+
// consumption meter at $0.50 each, and the catalog reports per-token prices of 0 for
|
|
94
|
+
// every route, so this table is the only price source pi can use.
|
|
95
|
+
// shortcut: IBM can reprice without a CLI release; re-measure if the footer looks wrong.
|
|
96
|
+
const BOBCOIN_USD = 0.5;
|
|
97
|
+
const DEFAULT_CREDITS_PER_MILLION = 2;
|
|
98
|
+
const CREDITS_PER_MILLION: Record<string, number> = {
|
|
99
|
+
premium: 2,
|
|
100
|
+
"premium-ide": 2,
|
|
101
|
+
"premium-shell": 2,
|
|
102
|
+
"wxO-model": 2,
|
|
103
|
+
background: 2,
|
|
104
|
+
security: 2,
|
|
105
|
+
fast: 5 / 6,
|
|
106
|
+
explorer: 5 / 6,
|
|
107
|
+
"gpt-oss-20b": 0,
|
|
108
|
+
"openai/gpt-oss-20b": 0,
|
|
109
|
+
};
|
|
110
|
+
|
|
90
111
|
function env(name: string): string | undefined {
|
|
91
112
|
const value = process.env[name]?.trim();
|
|
92
113
|
return value ? value : undefined;
|
|
@@ -103,6 +124,14 @@ function envOptionalInt(name: string): number | undefined {
|
|
|
103
124
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
104
125
|
}
|
|
105
126
|
|
|
127
|
+
function envOptionalFloat(name: string): number | undefined {
|
|
128
|
+
const value = env(name);
|
|
129
|
+
if (!value) return undefined;
|
|
130
|
+
// Number() rejects trailing junk that parseFloat would accept ("4abc", "0x10").
|
|
131
|
+
const parsed = Number(value);
|
|
132
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
106
135
|
function envBool(name: string, fallback: boolean): boolean {
|
|
107
136
|
const value = env(name);
|
|
108
137
|
if (!value) return fallback;
|
|
@@ -164,8 +193,13 @@ function configuredApiKey(): { envName: "IBM_BOB_API_KEY" | "IBM_BOB_KEY"; value
|
|
|
164
193
|
return alias ? { envName: "IBM_BOB_KEY", value: alias } : undefined;
|
|
165
194
|
}
|
|
166
195
|
|
|
167
|
-
function providerApiKeyReference(): string {
|
|
168
|
-
|
|
196
|
+
function providerApiKeyReference(): string | undefined {
|
|
197
|
+
const apiKey = configuredApiKey();
|
|
198
|
+
// Only advertise when resolvable at launch: an unresolvable $VAR reference makes
|
|
199
|
+
// pi's /model catalog refresh throw for a logged-out provider ("Could not refresh
|
|
200
|
+
// ibm-bob; showing cached models"). Process env is fixed after launch, so this
|
|
201
|
+
// loses nothing.
|
|
202
|
+
return apiKey ? `$${apiKey.envName}` : undefined;
|
|
169
203
|
}
|
|
170
204
|
|
|
171
205
|
function apiKeyAuthScheme(): string {
|
|
@@ -421,12 +455,31 @@ function cachedCatalog(credentials: OAuthCredentials | undefined): BobDiscovered
|
|
|
421
455
|
return sanitizeCachedCatalog((credentials as BobOAuthCredentials | undefined)?.bobModelCatalog);
|
|
422
456
|
}
|
|
423
457
|
|
|
458
|
+
// Bob's catalog prices every route at 0, so the Bobcoin rate table supplies the cost
|
|
459
|
+
// pi shows. Any bucket IBM does populate wins over the table.
|
|
460
|
+
function bobCost(
|
|
461
|
+
id: string,
|
|
462
|
+
catalogCost: BobModelConfig["cost"],
|
|
463
|
+
creditsOverride: number | undefined,
|
|
464
|
+
): BobModelConfig["cost"] {
|
|
465
|
+
const credits = creditsOverride ?? CREDITS_PER_MILLION[id] ?? DEFAULT_CREDITS_PER_MILLION;
|
|
466
|
+
const usdPerMillion = credits * BOBCOIN_USD;
|
|
467
|
+
// Bob charges cache reads and writes at the same flat rate as fresh input.
|
|
468
|
+
return {
|
|
469
|
+
input: catalogCost.input || usdPerMillion,
|
|
470
|
+
output: catalogCost.output || usdPerMillion,
|
|
471
|
+
cacheRead: catalogCost.cacheRead || usdPerMillion,
|
|
472
|
+
cacheWrite: catalogCost.cacheWrite || usdPerMillion,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
424
476
|
export function buildModels(api: CompatibleApi, discovered?: BobDiscoveredModel[]): BobModelConfig[] {
|
|
425
477
|
const reasoningModels = parseReasoningModels();
|
|
426
478
|
const reasoningOverride = envOptionalBool("IBM_BOB_REASONING");
|
|
427
479
|
const inputOverride = env("IBM_BOB_INPUT") ? parseInputTypes() : undefined;
|
|
428
480
|
const contextOverride = envOptionalInt("IBM_BOB_CONTEXT_WINDOW");
|
|
429
481
|
const maxTokensOverride = envOptionalInt("IBM_BOB_MAX_TOKENS");
|
|
482
|
+
const creditsOverride = envOptionalFloat("IBM_BOB_CREDITS_PER_MILLION");
|
|
430
483
|
const compat = buildOpenAiCompat(api) ?? buildAnthropicCompat(api);
|
|
431
484
|
|
|
432
485
|
if (discovered && discovered.length > 0) {
|
|
@@ -440,7 +493,7 @@ export function buildModels(api: CompatibleApi, discovered?: BobDiscoveredModel[
|
|
|
440
493
|
input: inputOverride ?? (model.supportsVision ? ["text", "image"] : ["text"]),
|
|
441
494
|
contextWindow: contextOverride ?? model.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
442
495
|
maxTokens: maxTokensOverride ?? model.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
443
|
-
cost: model.cost,
|
|
496
|
+
cost: bobCost(model.id, model.cost, creditsOverride),
|
|
444
497
|
...(compat ? { compat } : {}),
|
|
445
498
|
}));
|
|
446
499
|
}
|
|
@@ -457,7 +510,7 @@ export function buildModels(api: CompatibleApi, discovered?: BobDiscoveredModel[
|
|
|
457
510
|
input,
|
|
458
511
|
contextWindow,
|
|
459
512
|
maxTokens,
|
|
460
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
513
|
+
cost: bobCost(id, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, creditsOverride),
|
|
461
514
|
...(compat ? { compat } : {}),
|
|
462
515
|
}));
|
|
463
516
|
}
|
|
@@ -921,6 +974,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
921
974
|
const headers = buildHeaders(settings);
|
|
922
975
|
let catalog: BobDiscoveredModel[] | undefined;
|
|
923
976
|
const apiKey = configuredApiKey();
|
|
977
|
+
const apiKeyRef = providerApiKeyReference();
|
|
924
978
|
if (apiKey && envBool("IBM_BOB_DISCOVER_MODELS", true)) {
|
|
925
979
|
try {
|
|
926
980
|
catalog = await fetchBobModelCatalog(apiKey.value, apiKeyAuthScheme(), settings);
|
|
@@ -934,7 +988,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
934
988
|
pi.registerProvider(PROVIDER_ID, {
|
|
935
989
|
name: "IBM Bob",
|
|
936
990
|
baseUrl: providerRequestBaseUrl(api),
|
|
937
|
-
apiKey:
|
|
991
|
+
...(apiKeyRef ? { apiKey: apiKeyRef } : {}),
|
|
938
992
|
api: BOB_API,
|
|
939
993
|
...(headers ? { headers } : {}),
|
|
940
994
|
models: buildModels(api, catalog),
|