@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added public SDK exports for CLI-equivalent model and scoped-model resolution ([#6201](https://github.com/earendil-works/pi/issues/6201)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed oversized bash tool timeouts to fail with a clear validation error instead of being clamped to an immediate timeout ([#6181](https://github.com/earendil-works/pi/issues/6181)).
|
||||
|
||||
@@ -405,6 +405,32 @@ If no model is provided:
|
||||
2. Uses default from settings
|
||||
3. Falls back to first available model
|
||||
|
||||
To match CLI model parsing, use the exported resolver helpers:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
resolveCliModel,
|
||||
resolveModelScopeWithDiagnostics,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const cliModel = resolveCliModel({
|
||||
cliModel: "anthropic/claude-opus-4-5:high",
|
||||
modelRegistry,
|
||||
});
|
||||
if (cliModel.error) throw new Error(cliModel.error);
|
||||
if (cliModel.warning) console.warn(cliModel.warning);
|
||||
|
||||
const { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(
|
||||
["anthropic/*:high", "gpt-5"],
|
||||
modelRegistry,
|
||||
);
|
||||
for (const diagnostic of diagnostics) {
|
||||
console.warn(diagnostic.message);
|
||||
}
|
||||
```
|
||||
|
||||
`resolveCliModel()` uses all registered models so `--api-key` style first-time setup can resolve a model before stored auth exists. `resolveModelScopeWithDiagnostics()` matches `--models` and `enabledModels` semantics while returning warnings instead of printing them.
|
||||
|
||||
> See [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)
|
||||
|
||||
### API Keys and OAuth
|
||||
@@ -1104,6 +1130,8 @@ AgentSessionRuntime
|
||||
// Auth and Models
|
||||
AuthStorage
|
||||
ModelRegistry
|
||||
resolveCliModel
|
||||
resolveModelScopeWithDiagnostics
|
||||
|
||||
// Resource loading
|
||||
DefaultResourceLoader
|
||||
|
||||
@@ -255,9 +255,24 @@ export function parseModelPattern(
|
||||
* The algorithm tries to match the full pattern first, then progressively
|
||||
* strips colon-suffixes to find a match.
|
||||
*/
|
||||
export async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {
|
||||
export interface ModelScopeDiagnostic {
|
||||
type: "warning";
|
||||
message: string;
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
export interface ResolveModelScopeResult {
|
||||
scopedModels: ScopedModel[];
|
||||
diagnostics: ModelScopeDiagnostic[];
|
||||
}
|
||||
|
||||
export async function resolveModelScopeWithDiagnostics(
|
||||
patterns: string[],
|
||||
modelRegistry: ModelRegistry,
|
||||
): Promise<ResolveModelScopeResult> {
|
||||
const availableModels = await modelRegistry.getAvailable();
|
||||
const scopedModels: ScopedModel[] = [];
|
||||
const diagnostics: ModelScopeDiagnostic[] = [];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
// Check if pattern contains glob characters
|
||||
@@ -283,7 +298,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
||||
});
|
||||
|
||||
if (matchingModels.length === 0) {
|
||||
console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`));
|
||||
diagnostics.push({ type: "warning", message: `No models match pattern "${pattern}"`, pattern });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -298,11 +313,11 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
||||
const { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);
|
||||
|
||||
if (warning) {
|
||||
console.warn(chalk.yellow(`Warning: ${warning}`));
|
||||
diagnostics.push({ type: "warning", message: warning, pattern });
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`));
|
||||
diagnostics.push({ type: "warning", message: `No models match pattern "${pattern}"`, pattern });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -312,6 +327,14 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
||||
}
|
||||
}
|
||||
|
||||
return { scopedModels, diagnostics };
|
||||
}
|
||||
|
||||
export async function resolveModelScope(patterns: string[], modelRegistry: ModelRegistry): Promise<ScopedModel[]> {
|
||||
const { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(patterns, modelRegistry);
|
||||
for (const diagnostic of diagnostics) {
|
||||
console.warn(chalk.yellow(`Warning: ${diagnostic.message}`));
|
||||
}
|
||||
return scopedModels;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,14 @@ export {
|
||||
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts";
|
||||
export { convertToLlm } from "./core/messages.ts";
|
||||
export { ModelRegistry } from "./core/model-registry.ts";
|
||||
export {
|
||||
type ModelScopeDiagnostic,
|
||||
type ResolveCliModelResult,
|
||||
type ResolveModelScopeResult,
|
||||
resolveCliModel,
|
||||
resolveModelScopeWithDiagnostics,
|
||||
type ScopedModel,
|
||||
} from "./core/model-resolver.ts";
|
||||
export type {
|
||||
PackageManager,
|
||||
PathMetadata,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Model } from "@earendil-works/pi-ai";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
defaultModelPerProvider,
|
||||
findInitialModel,
|
||||
parseModelPattern,
|
||||
resolveCliModel,
|
||||
resolveModelScope,
|
||||
resolveModelScopeWithDiagnostics,
|
||||
} from "../src/core/model-resolver.ts";
|
||||
|
||||
// Mock models for testing
|
||||
@@ -206,6 +208,55 @@ describe("parseModelPattern", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveModelScopeWithDiagnostics", () => {
|
||||
test("returns scoped models and structured diagnostics without writing console warnings", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
const registry = {
|
||||
getAvailable: () => allModels,
|
||||
} as unknown as Parameters<typeof resolveModelScopeWithDiagnostics>[1];
|
||||
|
||||
const result = await resolveModelScopeWithDiagnostics(["sonnet:high", "gpt-4o:invalid", "missing"], registry);
|
||||
|
||||
expect(result.scopedModels.map((scoped) => scoped.model.id)).toEqual(["claude-sonnet-4-5", "gpt-4o"]);
|
||||
expect(result.scopedModels[0].thinkingLevel).toBe("high");
|
||||
expect(result.scopedModels[1].thinkingLevel).toBeUndefined();
|
||||
expect(result.diagnostics).toEqual([
|
||||
{
|
||||
type: "warning",
|
||||
message: 'Invalid thinking level "invalid" in pattern "gpt-4o:invalid". Using default instead.',
|
||||
pattern: "gpt-4o:invalid",
|
||||
},
|
||||
{
|
||||
type: "warning",
|
||||
message: 'No models match pattern "missing"',
|
||||
pattern: "missing",
|
||||
},
|
||||
]);
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveModelScope preserves CLI warning output", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
const registry = {
|
||||
getAvailable: () => allModels,
|
||||
} as unknown as Parameters<typeof resolveModelScope>[1];
|
||||
|
||||
const scopedModels = await resolveModelScope(["missing"], registry);
|
||||
|
||||
expect(scopedModels).toEqual([]);
|
||||
expect(warn).toHaveBeenCalledOnce();
|
||||
expect(warn.mock.calls[0][0]).toContain('Warning: No models match pattern "missing"');
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCliModel", () => {
|
||||
test("resolves --model provider/id without --provider", () => {
|
||||
const registry = {
|
||||
|
||||
Reference in New Issue
Block a user