@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
||||||
|
|
||||||
- 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)).
|
- 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
|
2. Uses default from settings
|
||||||
3. Falls back to first available model
|
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)
|
> See [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)
|
||||||
|
|
||||||
### API Keys and OAuth
|
### API Keys and OAuth
|
||||||
@@ -1104,6 +1130,8 @@ AgentSessionRuntime
|
|||||||
// Auth and Models
|
// Auth and Models
|
||||||
AuthStorage
|
AuthStorage
|
||||||
ModelRegistry
|
ModelRegistry
|
||||||
|
resolveCliModel
|
||||||
|
resolveModelScopeWithDiagnostics
|
||||||
|
|
||||||
// Resource loading
|
// Resource loading
|
||||||
DefaultResourceLoader
|
DefaultResourceLoader
|
||||||
|
|||||||
@@ -255,9 +255,24 @@ export function parseModelPattern(
|
|||||||
* The algorithm tries to match the full pattern first, then progressively
|
* The algorithm tries to match the full pattern first, then progressively
|
||||||
* strips colon-suffixes to find a match.
|
* 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 availableModels = await modelRegistry.getAvailable();
|
||||||
const scopedModels: ScopedModel[] = [];
|
const scopedModels: ScopedModel[] = [];
|
||||||
|
const diagnostics: ModelScopeDiagnostic[] = [];
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
// Check if pattern contains glob characters
|
// Check if pattern contains glob characters
|
||||||
@@ -283,7 +298,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchingModels.length === 0) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,11 +313,11 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
|
|||||||
const { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);
|
const { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);
|
||||||
|
|
||||||
if (warning) {
|
if (warning) {
|
||||||
console.warn(chalk.yellow(`Warning: ${warning}`));
|
diagnostics.push({ type: "warning", message: warning, pattern });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`));
|
diagnostics.push({ type: "warning", message: `No models match pattern "${pattern}"`, pattern });
|
||||||
continue;
|
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;
|
return scopedModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,14 @@ export {
|
|||||||
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts";
|
export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts";
|
||||||
export { convertToLlm } from "./core/messages.ts";
|
export { convertToLlm } from "./core/messages.ts";
|
||||||
export { ModelRegistry } from "./core/model-registry.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 {
|
export type {
|
||||||
PackageManager,
|
PackageManager,
|
||||||
PathMetadata,
|
PathMetadata,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { Model } from "@earendil-works/pi-ai";
|
import type { Model } from "@earendil-works/pi-ai";
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
defaultModelPerProvider,
|
defaultModelPerProvider,
|
||||||
findInitialModel,
|
findInitialModel,
|
||||||
parseModelPattern,
|
parseModelPattern,
|
||||||
resolveCliModel,
|
resolveCliModel,
|
||||||
|
resolveModelScope,
|
||||||
|
resolveModelScopeWithDiagnostics,
|
||||||
} from "../src/core/model-resolver.ts";
|
} from "../src/core/model-resolver.ts";
|
||||||
|
|
||||||
// Mock models for testing
|
// 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", () => {
|
describe("resolveCliModel", () => {
|
||||||
test("resolves --model provider/id without --provider", () => {
|
test("resolves --model provider/id without --provider", () => {
|
||||||
const registry = {
|
const registry = {
|
||||||
|
|||||||
Reference in New Issue
Block a user