Skip to content

Commit 120d664

Browse files
committed
docs(budget-model): rewrite README and JSDoc for nested auth shape
- README usage examples updated from `{ model, apiKey }` to `{ model, auth }` with `{ ...auth }` spread idiom. - New section documenting both forms of `modelOverride` (string vs object), including the auth-bypass use case. - Restored JSDoc on `BudgetModel` interface explaining the spread pattern. - Expanded `findBudgetModel` JSDoc to describe override semantics. - Added auth.test.ts case for object form with empty auth (`auth: {}`), confirming SDK-resolved-credentials use case isn't rejected.
1 parent f17950b commit 120d664

4 files changed

Lines changed: 85 additions & 13 deletions

File tree

.changeset/align-pi-versions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"pi-bash-bg": patch
3+
"pi-bash-trim": patch
4+
"pi-desktop-notify": patch
5+
"pi-enclave": patch
6+
"pi-jujutsu": patch
7+
"pi-no-soft-cursor": patch
8+
---
9+
10+
Align `@mariozechner/pi-coding-agent` (and `@mariozechner/pi-tui` for `pi-no-soft-cursor`) `devDependency` to `^0.63.0`.
11+
12+
`devDependency`-only update; no runtime change. Published `dist/*.d.ts` may reference newer upstream types from pi 0.63 (e.g. the `signal` property on `ExtensionContext`), so consumers writing extensions against these packages should be on pi ≥ 0.63 to match. Required to keep `tsup --dts` working alongside `pi-budget-model`, which now depends on the 0.63 registry API.

packages/budget-model/README.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,44 @@ npm install pi-budget-model
1313
## Usage
1414

1515
```typescript
16+
import { completeSimple } from "@mariozechner/pi-ai";
1617
import { findBudgetModel, NoBudgetModelError } from "pi-budget-model";
1718

1819
// Simple — cheapest in same provider, latest major version
19-
const { model, apiKey } = await findBudgetModel(ctx);
20+
const { model, auth } = await findBudgetModel(ctx);
2021

21-
// Cross-provider — cheapest across all providers with an API key
22-
const { model, apiKey } = await findBudgetModel(ctx, {
22+
// `auth` is `{ apiKey?: string; headers?: Record<string, string> }`, ready to
23+
// spread into pi-ai's stream/completeSimple options.
24+
await completeSimple(model, ctx, { ...auth, signal });
25+
26+
// Cross-provider — cheapest across all providers with usable auth
27+
const { model, auth } = await findBudgetModel(ctx, {
2328
strategy: "any-provider",
2429
});
2530

2631
// Include previous major version (often much cheaper)
27-
const { model, apiKey } = await findBudgetModel(ctx, {
32+
const { model, auth } = await findBudgetModel(ctx, {
2833
majorVersions: 2, // latest + previous major version
2934
costRatio: 0.5, // must be ≤ half the active model's cost
3035
});
3136

3237
// Pin a specific model, skip auto-selection
33-
const { model, apiKey } = await findBudgetModel(ctx, {
38+
const { model, auth } = await findBudgetModel(ctx, {
3439
modelOverride: "anthropic/claude-haiku-4-5",
3540
});
3641

42+
// Pin a model AND supply credentials directly, bypassing the registry's auth
43+
// resolution. Use as an escape hatch when the registry's auth pipeline
44+
// misbehaves for your provider.
45+
const { model, auth } = await findBudgetModel(ctx, {
46+
modelOverride: {
47+
model: "openai/gpt-4o-mini",
48+
auth: { apiKey: process.env.OPENAI_API_KEY },
49+
},
50+
});
51+
3752
// Only free models
38-
const { model, apiKey } = await findBudgetModel(ctx, {
53+
const { model, auth } = await findBudgetModel(ctx, {
3954
costRatio: 0,
4055
});
4156
```
@@ -67,7 +82,7 @@ When the active model is already the cheapest (like Haiku 4.5), you can widen th
6782

6883
| Option | Type | Default | Description |
6984
|--------|------|---------|-------------|
70-
| `modelOverride` | `"provider/model-id"` || Pin a specific model, bypassing auto-selection entirely |
85+
| `modelOverride` | `"provider/model-id"` \| `{ model, auth }` || Pin a specific model, bypassing auto-selection entirely |
7186
| `strategy` | `"same-provider"` \| `"any-provider"` | `"same-provider"` | Where to search for budget models |
7287
| `costRatio` | `0``1` | `0.5` | Max cost as fraction of active model (0 = free only) |
7388
| `majorVersions` | `0`, `1`, `2`, ... | `1` | How many major versions to search (1 = latest, 2 = latest + previous, 0 = all) |
@@ -76,9 +91,12 @@ Options are validated at runtime with [valibot](https://valibot.dev). Invalid va
7691

7792
### `modelOverride`
7893

79-
When set, **all other options are ignored**. The model is resolved directly from the registry by provider and ID — no strategy, cost ratio, or version filtering applies. Throws `NoBudgetModelError` if the model doesn't exist or has no API key.
94+
When set, **all other options are ignored**. The model is resolved directly from the registry by provider and ID — no strategy, cost ratio, or version filtering applies. Throws `NoBudgetModelError` if the model doesn't exist in the registry.
95+
96+
Two forms:
8097

81-
This is useful for pinning a known-good model in config files, or for testing.
98+
- **String** `"provider/model-id"`: registry resolves both the model metadata and the auth credentials. Use this for pinning a known-good model in config files or for testing.
99+
- **Object** `{ model: "provider/model-id", auth: { apiKey?, headers? } }`: registry resolves the model metadata via `find()`, but auth is taken straight from the option — the registry's auth pipeline (`getApiKeyAndHeaders`) is not invoked. Use this as an escape hatch when the registry's auth resolution misbehaves for your provider, or to inject credentials from a non-pi source.
82100

83101
### Reusable options schema
84102

@@ -104,7 +122,7 @@ For `same-provider` (default):
104122
3. Find the cheapest model(s) by input cost
105123
4. Verify the cheapest is within the cost ratio vs the active model
106124
5. Among ties: higher version numbers win, aliases preferred over dated snapshots
107-
6. Return the first candidate with an available API key
125+
6. Return the first candidate the registry can authenticate (any `ok: true` result, including header-only and SDK-resolved auth)
108126

109127
For `any-provider`: same steps but across all providers, grouped independently per provider then merged.
110128

@@ -114,7 +132,7 @@ Throws `NoBudgetModelError` when no suitable model is found. The error includes
114132

115133
```typescript
116134
try {
117-
const { model, apiKey } = await findBudgetModel(ctx);
135+
const { model, auth } = await findBudgetModel(ctx);
118136
} catch (err) {
119137
if (err instanceof NoBudgetModelError) {
120138
err.reason; // why it failed

packages/budget-model/src/index.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,17 @@ export const BudgetModelOptions = v.object({
6666
});
6767
export type BudgetModelOptions = v.InferOutput<typeof BudgetModelOptions>;
6868

69-
/** A model selected for a background task, plus the auth material needed to call it. */
69+
/**
70+
* A model selected for a background task, plus the auth material needed to call it.
71+
*
72+
* `auth` is the same shape `pi-ai`'s `StreamOptions` accepts. The intended call
73+
* pattern is to spread it directly:
74+
*
75+
* ```ts
76+
* const { model, auth } = await findBudgetModel(ctx);
77+
* await completeSimple(model, ctx, { ...auth, signal, maxTokens });
78+
* ```
79+
*/
7080
export interface BudgetModel {
7181
model: Model<Api>;
7282
auth: BudgetModelAuth;
@@ -131,8 +141,18 @@ export class NoBudgetModelError extends Error {
131141
/**
132142
* Find the cheapest available model for background tasks.
133143
*
144+
* If `options.modelOverride` is set, selection is skipped:
145+
* - String form `"provider/model-id"`: registry resolves both model and auth.
146+
* - Object form `{ model, auth }`: registry resolves only the model metadata
147+
* via `find()`; auth is taken from the option, bypassing the registry's
148+
* auth pipeline. Useful as an escape hatch when registry auth misbehaves.
149+
*
150+
* Otherwise the configured `strategy` (`"same-provider"` or `"any-provider"`)
151+
* walks candidate models cheapest-first, gated by `costRatio` against the
152+
* active model, and returns the first candidate the registry can authenticate.
153+
*
134154
* @param ctx - Extension context
135-
* @param options - Strategy, cost ratio, and major version depth (validated at runtime)
155+
* @param options - Strategy, cost ratio, major version depth, and optional override (validated at runtime)
136156
* @throws NoBudgetModelError if no suitable model is found
137157
*/
138158
export async function findBudgetModel(ctx: ExtensionContext, options?: BudgetModelOptions): Promise<BudgetModel> {

packages/budget-model/test/auth.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,28 @@ describe("findBudgetModel modelOverride object form", () => {
170170
).rejects.toThrow(/not found in registry/);
171171
});
172172

173+
it("accepts an empty auth object (caller defers auth to ambient credentials)", async () => {
174+
// `BudgetModelAuth` has both fields optional. A caller using SDK-resolved
175+
// credentials (e.g. AWS Bedrock with profile-based auth) may legitimately
176+
// supply `auth: {}` — the downstream provider in pi-ai handles auth itself.
177+
// We assert that's not rejected.
178+
const all = await loadModels();
179+
const override = all[0];
180+
const ctx = makeCtx(
181+
all,
182+
() => {
183+
throw new Error("registry auth pipeline must not be invoked for object-form modelOverride");
184+
},
185+
undefined,
186+
);
187+
188+
const result = await findBudgetModel(ctx, {
189+
modelOverride: { model: `${String(override.provider)}/${override.id}`, auth: {} },
190+
});
191+
192+
expect(result.auth).toEqual({});
193+
});
194+
173195
it("string form still works (registry resolves both model and auth)", async () => {
174196
const all = await loadModels();
175197
const override = all[0];

0 commit comments

Comments
 (0)