Release v0.27.8 - OAuth takes priority over settings.json API keys

This commit is contained in:
Mario Zechner 2025-12-24 20:52:22 +01:00
parent 60768b90f3
commit a965b6f160
14 changed files with 426 additions and 102 deletions

40
package-lock.json generated
View file

@ -6447,11 +6447,11 @@
},
"packages/agent": {
"name": "@mariozechner/pi-agent-core",
"version": "0.27.7",
"version": "0.27.8",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-tui": "^0.27.7"
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-tui": "^0.27.8"
},
"devDependencies": {
"@types/node": "^24.3.0",
@ -6481,7 +6481,7 @@
},
"packages/ai": {
"name": "@mariozechner/pi-ai",
"version": "0.27.7",
"version": "0.27.8",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "0.71.2",
@ -6523,12 +6523,12 @@
},
"packages/coding-agent": {
"name": "@mariozechner/pi-coding-agent",
"version": "0.27.7",
"version": "0.27.8",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-agent-core": "^0.27.7",
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-tui": "^0.27.7",
"@mariozechner/pi-agent-core": "^0.27.8",
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-tui": "^0.27.8",
"chalk": "^5.5.0",
"cli-highlight": "^2.1.11",
"diff": "^8.0.2",
@ -6569,13 +6569,13 @@
},
"packages/mom": {
"name": "@mariozechner/pi-mom",
"version": "0.27.7",
"version": "0.27.8",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sandbox-runtime": "^0.0.16",
"@mariozechner/pi-agent-core": "^0.27.7",
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-coding-agent": "^0.27.7",
"@mariozechner/pi-agent-core": "^0.27.8",
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-coding-agent": "^0.27.8",
"@sinclair/typebox": "^0.34.0",
"@slack/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0",
@ -6614,10 +6614,10 @@
},
"packages/pods": {
"name": "@mariozechner/pi",
"version": "0.27.7",
"version": "0.27.8",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-agent-core": "^0.27.7",
"@mariozechner/pi-agent-core": "^0.27.8",
"chalk": "^5.5.0"
},
"bin": {
@ -6630,7 +6630,7 @@
},
"packages/proxy": {
"name": "@mariozechner/pi-proxy",
"version": "0.27.7",
"version": "0.27.8",
"dependencies": {
"@hono/node-server": "^1.14.0",
"hono": "^4.6.16"
@ -6646,7 +6646,7 @@
},
"packages/tui": {
"name": "@mariozechner/pi-tui",
"version": "0.27.7",
"version": "0.27.8",
"license": "MIT",
"dependencies": {
"@types/mime-types": "^2.1.4",
@ -6690,12 +6690,12 @@
},
"packages/web-ui": {
"name": "@mariozechner/pi-web-ui",
"version": "0.27.7",
"version": "0.27.8",
"license": "MIT",
"dependencies": {
"@lmstudio/sdk": "^1.5.0",
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-tui": "^0.27.7",
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-tui": "^0.27.8",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lucide": "^0.544.0",
@ -6716,7 +6716,7 @@
},
"packages/web-ui/example": {
"name": "pi-web-ui-example",
"version": "1.15.7",
"version": "1.15.8",
"dependencies": {
"@mariozechner/mini-lit": "^0.2.0",
"@mariozechner/pi-ai": "file:../../ai",

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-agent-core",
"version": "0.27.7",
"version": "0.27.8",
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module",
"main": "./dist/index.js",
@ -18,8 +18,8 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-tui": "^0.27.7"
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-tui": "^0.27.8"
},
"keywords": [
"ai",

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-ai",
"version": "0.27.7",
"version": "0.27.8",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
"main": "./dist/index.js",

View file

@ -2919,8 +2919,8 @@ export const MODELS = {
cost: {
input: 0.3,
output: 1.2,
cacheRead: 0,
cacheWrite: 0,
cacheRead: 0.03,
cacheWrite: 0.375,
},
contextWindow: 204800,
maxTokens: 131072,
@ -6359,23 +6359,6 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-70b-instruct": {
id: "meta-llama/llama-3.1-70b-instruct",
name: "Meta: Llama 3.1 70B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.39999999999999997,
output: 0.39999999999999997,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
@ -6410,6 +6393,23 @@ export const MODELS = {
contextWindow: 10000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-70b-instruct": {
id: "meta-llama/llama-3.1-70b-instruct",
name: "Meta: Llama 3.1 70B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.39999999999999997,
output: 0.39999999999999997,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-nemo": {
id: "mistralai/mistral-nemo",
name: "Mistral: Mistral Nemo",
@ -6546,6 +6546,23 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o": {
id: "openai/gpt-4o",
name: "OpenAI: GPT-4o",
@ -6580,23 +6597,6 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 64000,
} satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct",
@ -6716,23 +6716,6 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-turbo-preview": {
id: "openai/gpt-4-turbo-preview",
name: "OpenAI: GPT-4 Turbo Preview",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo-0613": {
id: "openai/gpt-3.5-turbo-0613",
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
@ -6750,6 +6733,23 @@ export const MODELS = {
contextWindow: 4095,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-turbo-preview": {
id: "openai/gpt-4-turbo-preview",
name: "OpenAI: GPT-4 Turbo Preview",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-tiny": {
id: "mistralai/mistral-tiny",
name: "Mistral Tiny",

View file

@ -2,6 +2,12 @@
## [Unreleased]
## [0.27.8] - 2025-12-24
### Fixed
- **API key priority**: OAuth tokens now take priority over settings.json API keys. Previously, an API key in settings.json would trump OAuth, causing users logged in with a plan (unlimited tokens) to be billed via PAYG instead.
## [0.27.7] - 2025-12-24
### Fixed

View file

@ -0,0 +1,314 @@
# Hooks v2: Commands + Context Control
Extends hooks with slash commands and context manipulation primitives.
## Goals
1. Hooks can register slash commands (`/pop`, `/pr`, `/test`)
2. Hooks can save custom session entries
3. Hooks can transform context before it goes to LLM
4. All handlers get unified baseline access to state
Benchmark: `/pop` (session stacking) implementable entirely as a hook.
## API Extensions
### Commands
```typescript
pi.command("pop", {
description: "Pop to previous turn",
handler: async (ctx) => {
// ctx has full access (see Unified Context below)
const selected = await ctx.ui.select("Pop to:", options);
// ...
return { status: "Done" }; // show status
return "prompt text"; // send to agent
return; // do nothing
}
});
```
### Custom Entries
```typescript
// Save arbitrary entry to session
await ctx.saveEntry({
type: "stack_pop", // custom type, ignored by core
backToIndex: 5,
summary: "...",
timestamp: Date.now()
});
```
### Context Transform
```typescript
// Fires when building context for LLM
pi.on("context", (event, ctx) => {
// event.entries: all session entries (including custom types)
// event.messages: core-computed messages (after compaction)
// Return modified messages, or undefined to keep default
return { messages: transformed };
});
```
Multiple `context` handlers chain: each receives previous handler's output.
### Rebuild Trigger
```typescript
// Force context rebuild (after saving entries)
await ctx.rebuildContext();
```
## Unified Context
All handlers receive:
```typescript
interface HookEventContext {
// Existing
exec(cmd: string, args: string[], opts?): Promise<ExecResult>;
ui: { select, confirm, input, notify };
hasUI: boolean;
cwd: string;
sessionFile: string | null;
// New: State (read-only)
model: Model<any> | null;
thinkingLevel: ThinkingLevel;
entries: readonly SessionEntry[];
messages: readonly AppMessage[];
// New: Utilities
findModel(provider: string, id: string): Model<any> | null;
availableModels(): Promise<Model<any>[]>;
resolveApiKey(model: Model<any>): Promise<string | undefined>;
// New: Mutation (commands only? or all?)
saveEntry(entry: { type: string; [k: string]: unknown }): Promise<void>;
rebuildContext(): Promise<void>;
}
```
Commands additionally get:
- `args: string[]`, `argsRaw: string`
- `setModel()`, `setThinkingLevel()` (state mutation)
## Benchmark: Stacking as Hook
```typescript
export default function(pi: HookAPI) {
// Command: /pop
pi.command("pop", {
description: "Pop to previous turn, summarizing substack",
handler: async (ctx) => {
// 1. Build turn list from entries
const turns = ctx.entries
.map((e, i) => ({ e, i }))
.filter(({ e }) => e.type === "message" && e.message.role === "user")
.map(({ e, i }) => ({ index: i, text: e.message.content.slice(0, 50) }));
if (!turns.length) return { status: "No turns to pop" };
// 2. User selects
const selected = await ctx.ui.select("Pop to:", turns.map(t => t.text));
if (!selected) return;
const backTo = turns.find(t => t.text === selected)!.index;
// 3. Summarize entries from backTo to now
const toSummarize = ctx.entries.slice(backTo)
.filter(e => e.type === "message")
.map(e => e.message);
const summary = await generateSummary(toSummarize, ctx);
// 4. Save custom entry
await ctx.saveEntry({
type: "stack_pop",
backToIndex: backTo,
summary,
timestamp: Date.now()
});
// 5. Rebuild
await ctx.rebuildContext();
return { status: "Popped stack" };
}
});
// Context transform: apply stack pops
pi.on("context", (event, ctx) => {
const pops = event.entries.filter(e => e.type === "stack_pop");
if (!pops.length) return; // use default
// Build exclusion set
const excluded = new Set<number>();
const summaryAt = new Map<number, string>();
for (const pop of pops) {
const popIdx = event.entries.indexOf(pop);
for (let i = pop.backToIndex; i <= popIdx; i++) excluded.add(i);
summaryAt.set(pop.backToIndex, pop.summary);
}
// Build filtered messages
const messages: AppMessage[] = [];
for (let i = 0; i < event.entries.length; i++) {
if (excluded.has(i)) continue;
if (summaryAt.has(i)) {
messages.push({
role: "user",
content: `[Subtask completed]\n\n${summaryAt.get(i)}`,
timestamp: Date.now()
});
}
const e = event.entries[i];
if (e.type === "message") messages.push(e.message);
}
return { messages };
});
}
async function generateSummary(messages, ctx) {
const apiKey = await ctx.resolveApiKey(ctx.model);
// Call LLM for summary...
}
```
## Core Changes Required
### session-manager.ts
```typescript
// Allow saving arbitrary entries
saveEntry(entry: { type: string; [k: string]: unknown }): void {
if (!entry.type) throw new Error("Entry must have type");
this.inMemoryEntries.push(entry);
this._persist(entry);
}
// buildSessionContext ignores unknown types (existing behavior works)
```
### hooks/types.ts
```typescript
// New event
interface ContextEvent {
type: "context";
entries: readonly SessionEntry[];
messages: AppMessage[];
}
// Extended base context (see Unified Context above)
// Command types
interface CommandOptions {
description?: string;
handler: (ctx: CommandContext) => Promise<CommandResult | void>;
}
type CommandResult =
| string
| { prompt: string; attachments?: Attachment[] }
| { status: string };
```
### hooks/loader.ts
```typescript
// Track registered commands
interface LoadedHook {
path: string;
handlers: Map<string, Handler[]>;
commands: Map<string, CommandOptions>; // NEW
}
// createHookAPI adds command() method
```
### hooks/runner.ts
```typescript
class HookRunner {
// State callbacks (set by AgentSession)
setStateCallbacks(cb: StateCallbacks): void;
// Command invocation
getCommands(): Map<string, CommandOptions>;
invokeCommand(name: string, argsRaw: string): Promise<CommandResult | void>;
// Context event with chaining
async emitContext(entries, messages): Promise<AppMessage[]> {
let result = messages;
for (const hook of this.hooks) {
const handlers = hook.handlers.get("context");
for (const h of handlers ?? []) {
const out = await h({ entries, messages: result }, this.createContext());
if (out?.messages) result = out.messages;
}
}
return result;
}
}
```
### agent-session.ts
```typescript
// Expose saveEntry
async saveEntry(entry): Promise<void> {
this.sessionManager.saveEntry(entry);
}
// Rebuild context
async rebuildContext(): Promise<void> {
const base = this.sessionManager.buildSessionContext();
const entries = this.sessionManager.getEntries();
const messages = await this._hookRunner.emitContext(entries, base.messages);
this.agent.replaceMessages(messages);
}
// Fire context event during normal context building too
```
### interactive-mode.ts
```typescript
// In setupEditorSubmitHandler, check hook commands
const commands = this.session.hookRunner?.getCommands();
if (commands?.has(commandName)) {
const result = await this.session.invokeCommand(commandName, argsRaw);
// Handle result...
return;
}
// Add hook commands to autocomplete
```
## Open Questions
1. **Mutation in all handlers or commands only?**
- `saveEntry`/`rebuildContext` in all handlers = more power, more footguns
- Commands only = safer, but limits hook creativity
- Recommendation: start with commands only
2. **Context event timing**
- Fire on every prompt? Or only when explicitly rebuilt?
- Need to fire on session load too
- Recommendation: fire whenever agent.replaceMessages is called
3. **Compaction interaction**
- Core compaction runs first, then `context` event
- Hooks can post-process compacted output
- Future: compaction itself could become a replaceable hook
4. **Multiple context handlers**
- Chain in load order (global → project)
- Each sees previous output
- No explicit priority system (KISS)

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-coding-agent",
"version": "0.27.7",
"version": "0.27.8",
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"piConfig": {
@ -39,9 +39,9 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/pi-agent-core": "^0.27.7",
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-tui": "^0.27.7",
"@mariozechner/pi-agent-core": "^0.27.8",
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-tui": "^0.27.8",
"chalk": "^5.5.0",
"cli-highlight": "^2.1.11",
"diff": "^8.0.2",

View file

@ -328,21 +328,25 @@ export function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlas
/**
* Create the default API key resolver.
* Priority: settings.json apiKeys > custom providers (models.json) > OAuth > environment variables.
* Priority: OAuth > custom providers (models.json) > environment variables > settings.json apiKeys.
*
* OAuth takes priority so users logged in with a plan (e.g. unlimited tokens) aren't
* accidentally billed via a PAYG API key sitting in settings.json.
*/
export function defaultGetApiKey(
settingsManager?: SettingsManager,
): (model: Model<any>) => Promise<string | undefined> {
return async (model: Model<any>) => {
// Check settings.json apiKeys first
if (settingsManager) {
const settingsKey = settingsManager.getApiKey(model.provider);
if (settingsKey) {
return settingsKey;
}
// Check OAuth, custom providers, env vars first
const resolvedKey = await getApiKeyForModel(model);
if (resolvedKey) {
return resolvedKey;
}
// Fall back to existing resolution (custom providers, OAuth, env vars)
return getApiKeyForModel(model);
// Fall back to settings.json apiKeys
if (settingsManager) {
return settingsManager.getApiKey(model.provider);
}
return undefined;
};
}

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-mom",
"version": "0.27.7",
"version": "0.27.8",
"description": "Slack bot that delegates messages to the pi coding agent",
"type": "module",
"bin": {
@ -21,9 +21,9 @@
},
"dependencies": {
"@anthropic-ai/sandbox-runtime": "^0.0.16",
"@mariozechner/pi-agent-core": "^0.27.7",
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-coding-agent": "^0.27.7",
"@mariozechner/pi-agent-core": "^0.27.8",
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-coding-agent": "^0.27.8",
"@sinclair/typebox": "^0.34.0",
"@slack/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi",
"version": "0.27.7",
"version": "0.27.8",
"description": "CLI tool for managing vLLM deployments on GPU pods",
"type": "module",
"bin": {
@ -34,7 +34,7 @@
"node": ">=20.0.0"
},
"dependencies": {
"@mariozechner/pi-agent-core": "^0.27.7",
"@mariozechner/pi-agent-core": "^0.27.8",
"chalk": "^5.5.0"
},
"devDependencies": {}

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-proxy",
"version": "0.27.7",
"version": "0.27.8",
"type": "module",
"description": "CORS and authentication proxy for pi-ai",
"main": "dist/index.js",

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-tui",
"version": "0.27.7",
"version": "0.27.8",
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
"type": "module",
"main": "dist/index.js",

View file

@ -1,6 +1,6 @@
{
"name": "pi-web-ui-example",
"version": "1.15.7",
"version": "1.15.8",
"private": true,
"type": "module",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-web-ui",
"version": "0.27.7",
"version": "0.27.8",
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
"type": "module",
"main": "dist/index.js",
@ -18,8 +18,8 @@
},
"dependencies": {
"@lmstudio/sdk": "^1.5.0",
"@mariozechner/pi-ai": "^0.27.7",
"@mariozechner/pi-tui": "^0.27.7",
"@mariozechner/pi-ai": "^0.27.8",
"@mariozechner/pi-tui": "^0.27.8",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lucide": "^0.544.0",