feat(coding-agent): replace model registry with model runtime
Move provider auth and OAuth flows onto pi-ai Models, compose models.json and extension overlays through ModelRuntime, and retain ModelRegistry as an extension compatibility facade.
This commit is contained in:
@@ -8,15 +8,8 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
||||
import {
|
||||
type AssistantMessage,
|
||||
getProviders,
|
||||
type ImageContent,
|
||||
type Message,
|
||||
type Model,
|
||||
type OAuthProviderId,
|
||||
type OAuthSelectPrompt,
|
||||
} from "@earendil-works/pi-ai/compat";
|
||||
import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai";
|
||||
import type { AssistantMessage, ImageContent, Message, Model } from "@earendil-works/pi-ai/compat";
|
||||
import type {
|
||||
AutocompleteItem,
|
||||
AutocompleteProvider,
|
||||
@@ -54,7 +47,6 @@ import {
|
||||
getAgentDir,
|
||||
getAuthPath,
|
||||
getDebugLogPath,
|
||||
getDocsPath,
|
||||
getShareViewerUrl,
|
||||
VERSION,
|
||||
} from "../../config.ts";
|
||||
@@ -85,7 +77,6 @@ import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.t
|
||||
import { createCompactionSummaryMessage } from "../../core/messages.ts";
|
||||
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.ts";
|
||||
import { DefaultPackageManager } from "../../core/package-manager.ts";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.ts";
|
||||
import type { ResourceDiagnostic } from "../../core/resource-loader.ts";
|
||||
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts";
|
||||
import { type SessionEntry, SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.ts";
|
||||
@@ -212,7 +203,7 @@ function isDeadTerminalError(error: unknown): boolean {
|
||||
}
|
||||
|
||||
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING =
|
||||
"Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
|
||||
"Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage. Disable this warning in /settings.";
|
||||
|
||||
function isAnthropicSubscriptionAuthKey(apiKey: string | undefined): boolean {
|
||||
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
||||
@@ -248,24 +239,6 @@ function hasDefaultModelProvider(providerId: string): providerId is keyof typeof
|
||||
return providerId in defaultModelPerProvider;
|
||||
}
|
||||
|
||||
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
|
||||
|
||||
const BUILT_IN_MODEL_PROVIDERS = new Set<string>(getProviders());
|
||||
|
||||
export function isApiKeyLoginProvider(
|
||||
providerId: string,
|
||||
oauthProviderIds: ReadonlySet<string>,
|
||||
builtInProviderIds: ReadonlySet<string> = BUILT_IN_MODEL_PROVIDERS,
|
||||
): boolean {
|
||||
if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
|
||||
return true;
|
||||
}
|
||||
if (builtInProviderIds.has(providerId)) {
|
||||
return false;
|
||||
}
|
||||
return !oauthProviderIds.has(providerId);
|
||||
}
|
||||
|
||||
type LoginProviderCompletionOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -571,12 +544,12 @@ export class InteractiveMode {
|
||||
|
||||
const modelCommand = slashCommands.find((command) => command.name === "model");
|
||||
if (modelCommand) {
|
||||
modelCommand.getArgumentCompletions = (prefix: string): AutocompleteItem[] | null => {
|
||||
modelCommand.getArgumentCompletions = async (prefix: string): Promise<AutocompleteItem[] | null> => {
|
||||
// Get available models (scoped or from registry)
|
||||
const models =
|
||||
this.session.scopedModels.length > 0
|
||||
? this.session.scopedModels.map((s) => s.model)
|
||||
: this.session.modelRegistry.getAvailable();
|
||||
: await this.session.modelRuntime.getAvailable();
|
||||
|
||||
if (models.length === 0) return null;
|
||||
|
||||
@@ -879,7 +852,7 @@ export class InteractiveMode {
|
||||
this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
|
||||
}
|
||||
|
||||
const modelsJsonError = this.session.modelRegistry.getError();
|
||||
const modelsJsonError = this.session.modelRuntime.getError();
|
||||
if (modelsJsonError) {
|
||||
this.showError(`models.json error: ${modelsJsonError}`);
|
||||
}
|
||||
@@ -1779,7 +1752,7 @@ export class InteractiveMode {
|
||||
hasUI: true,
|
||||
cwd: this.sessionManager.getCwd(),
|
||||
sessionManager: this.sessionManager,
|
||||
modelRegistry: this.session.modelRegistry,
|
||||
modelRegistry: extensionRunner.getModelRegistry(),
|
||||
model: this.session.model,
|
||||
isIdle: () => this.session.isIdle,
|
||||
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
|
||||
@@ -3288,7 +3261,7 @@ export class InteractiveMode {
|
||||
// Cache-miss notices are not persisted; re-derive them from the full entry
|
||||
// list and re-inject them after the assistant messages that paid for them.
|
||||
const cacheMisses = this.settingsManager.getShowCacheMissNotices()
|
||||
? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
|
||||
? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
|
||||
: new Map<AssistantMessage, CacheMiss>();
|
||||
|
||||
if (options.updateFooter) {
|
||||
@@ -3392,7 +3365,7 @@ export class InteractiveMode {
|
||||
if (!this.settingsManager.getShowCacheMissNotices()) return;
|
||||
|
||||
// Entries don't contain `message` yet: message_end fires before persistence.
|
||||
const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
|
||||
const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRuntime);
|
||||
if (miss) this.addCacheMissNotice(miss);
|
||||
}
|
||||
|
||||
@@ -4325,9 +4298,9 @@ export class InteractiveMode {
|
||||
return this.session.scopedModels.map((scoped) => scoped.model);
|
||||
}
|
||||
|
||||
this.session.modelRegistry.refresh();
|
||||
try {
|
||||
return await this.session.modelRegistry.getAvailable();
|
||||
await this.session.modelRuntime.refresh();
|
||||
return [...(await this.session.modelRuntime.getAvailable())];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -4353,15 +4326,13 @@ export class InteractiveMode {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedCredential = this.session.modelRegistry.authStorage.get("anthropic");
|
||||
if (storedCredential?.type === "oauth") {
|
||||
this.anthropicSubscriptionWarningShown = true;
|
||||
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
|
||||
if ((await this.session.modelRuntime.checkAuth("anthropic"))?.type === "oauth") {
|
||||
this.anthropicSubscriptionWarningShown = true;
|
||||
this.showWarning(ANTHROPIC_SUBSCRIPTION_AUTH_WARNING);
|
||||
return;
|
||||
}
|
||||
const apiKey = (await this.session.modelRuntime.getAuth(model.provider))?.auth.apiKey;
|
||||
if (!isAnthropicSubscriptionAuthKey(apiKey)) {
|
||||
return;
|
||||
}
|
||||
@@ -4429,7 +4400,7 @@ export class InteractiveMode {
|
||||
this.ui,
|
||||
this.session.model,
|
||||
this.settingsManager,
|
||||
this.session.modelRegistry,
|
||||
this.session.modelRuntime,
|
||||
this.session.scopedModels,
|
||||
async (model) => {
|
||||
try {
|
||||
@@ -4457,8 +4428,8 @@ export class InteractiveMode {
|
||||
|
||||
private async showModelsSelector(): Promise<void> {
|
||||
// Get all available models
|
||||
this.session.modelRegistry.refresh();
|
||||
const allModels = this.session.modelRegistry.getAvailable();
|
||||
await this.session.modelRuntime.refresh();
|
||||
const allModels = [...(await this.session.modelRuntime.getAvailable())];
|
||||
|
||||
if (allModels.length === 0) {
|
||||
this.showStatus("No models available");
|
||||
@@ -4479,7 +4450,7 @@ export class InteractiveMode {
|
||||
// Fall back to settings
|
||||
const patterns = this.settingsManager.getEnabledModels();
|
||||
if (patterns !== undefined && patterns.length > 0) {
|
||||
const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry);
|
||||
const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
|
||||
currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
|
||||
}
|
||||
}
|
||||
@@ -4488,7 +4459,7 @@ export class InteractiveMode {
|
||||
const updateSessionModels = async (enabledIds: string[] | null) => {
|
||||
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
|
||||
if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
|
||||
const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRegistry);
|
||||
const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
|
||||
this.session.setScopedModels(
|
||||
newScopedModels.map((sm) => ({
|
||||
model: sm.model,
|
||||
@@ -4790,48 +4761,46 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
private getLoginProviderOptions(authType?: "oauth" | "api_key"): AuthSelectorProvider[] {
|
||||
const authStorage = this.session.modelRegistry.authStorage;
|
||||
const oauthProviders = authStorage.getOAuthProviders();
|
||||
const oauthProviderIds = new Set(oauthProviders.map((provider) => provider.id));
|
||||
const options: AuthSelectorProvider[] = oauthProviders.map((provider) => ({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
authType: "oauth",
|
||||
}));
|
||||
|
||||
const modelProviders = new Set(this.session.modelRegistry.getAll().map((model) => model.provider));
|
||||
for (const providerId of modelProviders) {
|
||||
if (!isApiKeyLoginProvider(providerId, oauthProviderIds)) {
|
||||
continue;
|
||||
const options: AuthSelectorProvider[] = [];
|
||||
for (const provider of this.session.modelRuntime.getProviders()) {
|
||||
const authStatus = this.session.modelRuntime.getProviderAuthStatus(provider.id);
|
||||
const status = authStatus.configured
|
||||
? {
|
||||
type: this.session.modelRuntime.isUsingOAuth(provider.id) ? ("oauth" as const) : ("api_key" as const),
|
||||
source: authStatus.label ?? authStatus.source,
|
||||
}
|
||||
: undefined;
|
||||
if ((!authType || authType === "oauth") && provider.auth.oauth) {
|
||||
options.push({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
authType: "oauth",
|
||||
method: provider.auth.oauth,
|
||||
status,
|
||||
});
|
||||
}
|
||||
if ((!authType || authType === "api_key") && provider.auth.apiKey) {
|
||||
options.push({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
authType: "api_key",
|
||||
method: provider.auth.apiKey,
|
||||
status,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
id: providerId,
|
||||
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
||||
authType: "api_key",
|
||||
});
|
||||
}
|
||||
|
||||
const filteredOptions = authType ? options.filter((option) => option.authType === authType) : options;
|
||||
return filteredOptions.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return options.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
private getLogoutProviderOptions(): AuthSelectorProvider[] {
|
||||
const authStorage = this.session.modelRegistry.authStorage;
|
||||
const options: AuthSelectorProvider[] = [];
|
||||
|
||||
for (const providerId of authStorage.list()) {
|
||||
const credential = authStorage.get(providerId);
|
||||
if (!credential) {
|
||||
continue;
|
||||
}
|
||||
options.push({
|
||||
private async getLogoutProviderOptions(): Promise<AuthSelectorProvider[]> {
|
||||
return (await this.session.modelRuntime.listCredentials())
|
||||
.map(({ providerId, type }) => ({
|
||||
id: providerId,
|
||||
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
||||
authType: credential.type,
|
||||
});
|
||||
}
|
||||
|
||||
return options.sort((a, b) => a.name.localeCompare(b.name));
|
||||
name: this.session.modelRuntime.getProvider(providerId)?.name ?? providerId,
|
||||
authType: type,
|
||||
status: { type, source: "stored credential" },
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
private findLoginProviderOptions(providerRef: string): AuthSelectorProvider[] {
|
||||
@@ -4848,6 +4817,7 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
private async handleLoginCommand(providerRef?: string): Promise<void> {
|
||||
await this.session.modelRuntime.getAvailable();
|
||||
if (!providerRef) {
|
||||
this.showLoginAuthTypeSelector();
|
||||
return;
|
||||
@@ -4873,10 +4843,10 @@ export class InteractiveMode {
|
||||
private async startProviderLogin(providerOption: AuthSelectorProvider): Promise<void> {
|
||||
if (providerOption.authType === "oauth") {
|
||||
await this.showLoginDialog(providerOption.id, providerOption.name);
|
||||
} else if (providerOption.id === BEDROCK_PROVIDER_ID) {
|
||||
this.showBedrockSetupDialog(providerOption.id, providerOption.name);
|
||||
} else {
|
||||
} else if (providerOption.method?.login) {
|
||||
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
||||
} else {
|
||||
this.showAmbientAuthDialog(providerOption);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4951,7 +4921,6 @@ export class InteractiveMode {
|
||||
this.showSelector((done) => {
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
this.session.modelRegistry.authStorage,
|
||||
providerOptions,
|
||||
async (providerId, selectedAuthType) => {
|
||||
done();
|
||||
@@ -4973,7 +4942,6 @@ export class InteractiveMode {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
},
|
||||
(providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId),
|
||||
initialSearchInput,
|
||||
);
|
||||
return { component: selector, focus: selector };
|
||||
@@ -4986,7 +4954,7 @@ export class InteractiveMode {
|
||||
return;
|
||||
}
|
||||
|
||||
const providerOptions = this.getLogoutProviderOptions();
|
||||
const providerOptions = await this.getLogoutProviderOptions();
|
||||
if (providerOptions.length === 0) {
|
||||
this.showStatus(
|
||||
"No stored credentials to remove. /logout only removes credentials saved by /login; environment variables and models.json config are unchanged.",
|
||||
@@ -4997,7 +4965,6 @@ export class InteractiveMode {
|
||||
this.showSelector((done) => {
|
||||
const selector = new OAuthSelectorComponent(
|
||||
mode,
|
||||
this.session.modelRegistry.authStorage,
|
||||
providerOptions,
|
||||
async (providerId: string) => {
|
||||
done();
|
||||
@@ -5008,8 +4975,7 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
try {
|
||||
this.session.modelRegistry.authStorage.logout(providerOption.id);
|
||||
this.session.modelRegistry.refresh();
|
||||
await this.session.modelRuntime.logout(providerOption.id);
|
||||
await this.updateAvailableProviderCount();
|
||||
const message =
|
||||
providerOption.authType === "oauth"
|
||||
@@ -5035,14 +5001,14 @@ export class InteractiveMode {
|
||||
authType: "oauth" | "api_key",
|
||||
previousModel: Model<any> | undefined,
|
||||
): Promise<void> {
|
||||
this.session.modelRegistry.refresh();
|
||||
await this.session.modelRuntime.getAvailable();
|
||||
|
||||
const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
|
||||
|
||||
let selectedModel: Model<any> | undefined;
|
||||
let selectionError: string | undefined;
|
||||
if (isUnknownModel(previousModel)) {
|
||||
const availableModels = this.session.modelRegistry.getAvailable();
|
||||
const availableModels = await this.session.modelRuntime.getAvailable();
|
||||
const providerModels = availableModels.filter((model) => model.provider === providerId);
|
||||
if (!hasDefaultModelProvider(providerId)) {
|
||||
selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
|
||||
@@ -5082,7 +5048,7 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private showBedrockSetupDialog(providerId: string, providerName: string): void {
|
||||
private showAmbientAuthDialog(providerOption: AuthSelectorProvider): void {
|
||||
const restoreEditor = () => {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(this.editor);
|
||||
@@ -5092,17 +5058,12 @@ export class InteractiveMode {
|
||||
|
||||
const dialog = new LoginDialogComponent(
|
||||
this.ui,
|
||||
providerId,
|
||||
providerOption.id,
|
||||
() => restoreEditor(),
|
||||
providerName,
|
||||
"Amazon Bedrock setup",
|
||||
providerOption.name,
|
||||
`${providerOption.name} setup`,
|
||||
);
|
||||
dialog.showInfo([
|
||||
theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
|
||||
theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
|
||||
theme.fg("muted", "See:"),
|
||||
theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
|
||||
]);
|
||||
dialog.showInfo(`${providerOption.method?.name ?? "Authentication"} is configured outside pi.`, [], true);
|
||||
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(dialog);
|
||||
@@ -5135,13 +5096,7 @@ export class InteractiveMode {
|
||||
};
|
||||
|
||||
try {
|
||||
const apiKey = (await dialog.showPrompt("Enter API key:")).trim();
|
||||
if (!apiKey) {
|
||||
throw new Error("API key cannot be empty.");
|
||||
}
|
||||
|
||||
this.session.modelRegistry.authStorage.set(providerId, { type: "api_key", key: apiKey });
|
||||
|
||||
await this.loginProvider(dialog, providerId, "api_key");
|
||||
restoreEditor();
|
||||
await this.completeProviderAuthentication(providerId, providerName, "api_key", previousModel);
|
||||
} catch (error: unknown) {
|
||||
@@ -5153,8 +5108,11 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private showOAuthLoginSelect(dialog: LoginDialogComponent, prompt: OAuthSelectPrompt): Promise<string | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
private showAuthSelect(
|
||||
dialog: LoginDialogComponent,
|
||||
prompt: Extract<AuthPrompt, { type: "select" }>,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const restoreDialog = () => {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(dialog);
|
||||
@@ -5167,11 +5125,13 @@ export class InteractiveMode {
|
||||
labels,
|
||||
(optionLabel) => {
|
||||
restoreDialog();
|
||||
resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
|
||||
const id = prompt.options.find((option) => option.label === optionLabel)?.id;
|
||||
if (id) resolve(id);
|
||||
else reject(new Error("Login cancelled"));
|
||||
},
|
||||
() => {
|
||||
restoreDialog();
|
||||
resolve(undefined);
|
||||
reject(new Error("Login cancelled"));
|
||||
},
|
||||
);
|
||||
this.editorContainer.clear();
|
||||
@@ -5181,40 +5141,63 @@ export class InteractiveMode {
|
||||
});
|
||||
}
|
||||
|
||||
private async showAuthPrompt(dialog: LoginDialogComponent, prompt: AuthPrompt): Promise<string> {
|
||||
let response: Promise<string>;
|
||||
if (prompt.type === "select") {
|
||||
response = this.showAuthSelect(dialog, prompt);
|
||||
} else if (prompt.type === "manual_code") {
|
||||
response = dialog.showManualInput(prompt.message);
|
||||
} else {
|
||||
response = dialog.showPrompt(prompt.message, prompt.placeholder);
|
||||
}
|
||||
if (!prompt.signal) return response;
|
||||
if (prompt.signal.aborted) throw new Error("Login cancelled");
|
||||
const signal = prompt.signal;
|
||||
let onAbort: (() => void) | undefined;
|
||||
const aborted = new Promise<string>((_resolve, reject) => {
|
||||
onAbort = () => reject(new Error("Login cancelled"));
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
try {
|
||||
return await Promise.race([response, aborted]);
|
||||
} finally {
|
||||
if (onAbort) signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
private notifyAuthDialog(dialog: LoginDialogComponent, event: AuthEvent): void {
|
||||
if (event.type === "auth_url") {
|
||||
dialog.showAuth(event.url, event.instructions);
|
||||
} else if (event.type === "device_code") {
|
||||
dialog.showDeviceCode(event);
|
||||
dialog.showWaiting("Waiting for authentication...");
|
||||
} else if (event.type === "info") {
|
||||
dialog.showInfo(event.message, event.links);
|
||||
} else {
|
||||
dialog.showProgress(event.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async loginProvider(
|
||||
dialog: LoginDialogComponent,
|
||||
providerId: string,
|
||||
method: "api_key" | "oauth",
|
||||
): Promise<void> {
|
||||
await this.session.modelRuntime.login(providerId, method, {
|
||||
signal: dialog.signal,
|
||||
prompt: (prompt) => this.showAuthPrompt(dialog, prompt),
|
||||
notify: (event) => this.notifyAuthDialog(dialog, event),
|
||||
});
|
||||
}
|
||||
|
||||
private async showLoginDialog(providerId: string, providerName: string): Promise<void> {
|
||||
const providerInfo = this.session.modelRegistry.authStorage
|
||||
.getOAuthProviders()
|
||||
.find((provider) => provider.id === providerId);
|
||||
const previousModel = this.session.model;
|
||||
|
||||
// Providers that use callback servers (can paste redirect URL)
|
||||
const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
|
||||
|
||||
// Create login dialog component
|
||||
const dialog = new LoginDialogComponent(
|
||||
this.ui,
|
||||
providerId,
|
||||
(_success, _message) => {
|
||||
// Completion handled below
|
||||
},
|
||||
providerName,
|
||||
);
|
||||
|
||||
// Show dialog in editor container
|
||||
const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {}, providerName);
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(dialog);
|
||||
this.ui.setFocus(dialog);
|
||||
this.ui.requestRender();
|
||||
|
||||
// Promise for manual code input (racing with callback server)
|
||||
let manualCodeResolve: ((code: string) => void) | undefined;
|
||||
let manualCodeReject: ((err: Error) => void) | undefined;
|
||||
const manualCodePromise = new Promise<string>((resolve, reject) => {
|
||||
manualCodeResolve = resolve;
|
||||
manualCodeReject = reject;
|
||||
});
|
||||
|
||||
// Restore editor helper
|
||||
const restoreEditor = () => {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(this.editor);
|
||||
@@ -5223,51 +5206,7 @@ export class InteractiveMode {
|
||||
};
|
||||
|
||||
try {
|
||||
await this.session.modelRegistry.authStorage.login(providerId as OAuthProviderId, {
|
||||
onAuth: (info: { url: string; instructions?: string }) => {
|
||||
dialog.showAuth(info.url, info.instructions);
|
||||
|
||||
if (usesCallbackServer) {
|
||||
// Show input for manual paste, racing with callback
|
||||
dialog
|
||||
.showManualInput("Paste redirect URL below, or complete login in browser:")
|
||||
.then((value) => {
|
||||
if (value && manualCodeResolve) {
|
||||
manualCodeResolve(value);
|
||||
manualCodeResolve = undefined;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (manualCodeReject) {
|
||||
manualCodeReject(new Error("Login cancelled"));
|
||||
manualCodeReject = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
// For Anthropic: onPrompt is called immediately after
|
||||
},
|
||||
|
||||
onDeviceCode: (info) => {
|
||||
dialog.showDeviceCode(info);
|
||||
dialog.showWaiting("Waiting for authentication...");
|
||||
},
|
||||
|
||||
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
||||
return dialog.showPrompt(prompt.message, prompt.placeholder);
|
||||
},
|
||||
|
||||
onProgress: (message: string) => {
|
||||
dialog.showProgress(message);
|
||||
},
|
||||
|
||||
onSelect: (prompt: OAuthSelectPrompt) => this.showOAuthLoginSelect(dialog, prompt),
|
||||
|
||||
onManualCodeInput: () => manualCodePromise,
|
||||
|
||||
signal: dialog.signal,
|
||||
});
|
||||
|
||||
// Success
|
||||
await this.loginProvider(dialog, providerId, "oauth");
|
||||
restoreEditor();
|
||||
await this.completeProviderAuthentication(providerId, providerName, "oauth", previousModel);
|
||||
} catch (error: unknown) {
|
||||
@@ -5368,7 +5307,7 @@ export class InteractiveMode {
|
||||
showDiagnosticsWhenQuiet: true,
|
||||
});
|
||||
const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
|
||||
const modelsJsonError = this.session.modelRegistry.getError();
|
||||
const modelsJsonError = this.session.modelRuntime.getError();
|
||||
if (modelsJsonError) {
|
||||
this.showError(`models.json error: ${modelsJsonError}`);
|
||||
}
|
||||
@@ -5613,7 +5552,7 @@ export class InteractiveMode {
|
||||
const stats = this.session.getSessionStats();
|
||||
const sessionName = this.sessionManager.getSessionName();
|
||||
const entries = this.sessionManager.getEntries();
|
||||
const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
|
||||
const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
|
||||
|
||||
// Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
|
||||
// resolves to a concrete responseModel), sorted by cost descending.
|
||||
|
||||
Reference in New Issue
Block a user