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 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.1`, 2026-08-31).**
8
- > SSO login, token refresh, and live model discovery all work against Bob 2.0 — 13 models discovered, including `premium`, `premium-ide`, `ultra`, `fast`, and `wxO-model` (1M ctx). 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.
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
- Updated for the installed `bobshell@2.0.1` package (upgraded from 1.0.6; findings verified against the live endpoint on 2026-08-31):
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
- - **Breaking:** 2.x `model/info` entries dropped `litellm_params` (the backend identifier) and now carry `max_output_tokens` and `supports_prompt_caching` alongside `max_tokens`. 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
- - **Breaking:** 2.x `model/info` requires `x-instance-id` (HTTP 400 without it); `x-team-id` stays optional. The extension already 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` (`explorer` exists but is hidden). Live catalog also exposes `premium-ide`, `premium-shell`, `sonnet-4.5`, `wxO-model` (1M ctx / 64k out), `gpt-oss-20b`, and granite/rnj code models. The `premium` alias still exists, so the fallback catalog stays valid.
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-08-31).
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, and token prices are mapped into Pi model definitions. Bob reports prices per token; Pi displays prices per million tokens, so the extension performs that conversion.
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`, `rnj-1-*`) use the defaults: 200,000 context / 8,192 output, overridable via `IBM_BOB_CONTEXT_WINDOW` / `IBM_BOB_MAX_TOKENS`.
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
- Discovered pricing comes from Bob's model-info response and is converted to Pi's per-million-token units. Fallback models retain zero pricing.
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` shows all 13 catalog models (`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.
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
- return `$${configuredApiKey()?.envName ?? "IBM_BOB_API_KEY"}`;
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: providerApiKeyReference(),
991
+ ...(apiKeyRef ? { apiKey: apiKeyRef } : {}),
938
992
  api: BOB_API,
939
993
  ...(headers ? { headers } : {}),
940
994
  models: buildModels(api, catalog),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bob",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "description": "Pi provider extension for IBM Bob / IBM-approved compatible model endpoints.",
6
6
  "keywords": [