@@ -161,7 +161,7 @@ Create provider file exporting:
|
|||||||
### 6. Coding Agent (`packages/coding-agent/`)
|
### 6. Coding Agent (`packages/coding-agent/`)
|
||||||
|
|
||||||
- `src/core/model-resolver.ts`: Add default model ID to `defaultModelPerProvider`
|
- `src/core/model-resolver.ts`: Add default model ID to `defaultModelPerProvider`
|
||||||
- `src/modes/interactive/interactive-mode.ts`: Add API-key login display name to `API_KEY_LOGIN_PROVIDERS` so `/login` shows the provider for built-in API-key auth.
|
- `src/core/provider-display-names.ts`: Add API-key login display name so `/login` and related UI show the provider for built-in API-key auth.
|
||||||
- `src/cli/args.ts`: Add env var documentation
|
- `src/cli/args.ts`: Add env var documentation
|
||||||
- `README.md`: Add provider setup instructions
|
- `README.md`: Add provider setup instructions
|
||||||
- `docs/providers.md`: Add setup instructions, env var, and `auth.json` key
|
- `docs/providers.md`: Add setup instructions, env var, and `auth.json` key
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added top-level `name` support to `pi.registerProvider()` so extension-registered providers can show a friendly name in `/login` ([#3956](https://github.com/badlogic/pi-mono/issues/3956)).
|
||||||
- Added `ctx.ui.getEditorComponent()` so extensions can wrap the currently configured custom editor factory ([#3935](https://github.com/badlogic/pi-mono/issues/3935)).
|
- Added `ctx.ui.getEditorComponent()` so extensions can wrap the currently configured custom editor factory ([#3935](https://github.com/badlogic/pi-mono/issues/3935)).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
// Register new provider with models
|
// Register new provider with models
|
||||||
pi.registerProvider("my-provider", {
|
pi.registerProvider("my-provider", {
|
||||||
|
name: "My Provider",
|
||||||
baseUrl: "https://api.example.com",
|
baseUrl: "https://api.example.com",
|
||||||
apiKey: "MY_API_KEY",
|
apiKey: "MY_API_KEY",
|
||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
@@ -544,6 +545,9 @@ Run tests with your provider/model pairs to verify compatibility.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ProviderConfig {
|
interface ProviderConfig {
|
||||||
|
/** Display name for the provider in UI such as /login. */
|
||||||
|
name?: string;
|
||||||
|
|
||||||
/** API endpoint URL. Required when defining models. */
|
/** API endpoint URL. Required when defining models. */
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1515,6 +1515,7 @@ If you need to discover models from a remote endpoint, prefer an async extension
|
|||||||
```typescript
|
```typescript
|
||||||
// Register a new provider with custom models
|
// Register a new provider with custom models
|
||||||
pi.registerProvider("my-proxy", {
|
pi.registerProvider("my-proxy", {
|
||||||
|
name: "My Proxy",
|
||||||
baseUrl: "https://proxy.example.com",
|
baseUrl: "https://proxy.example.com",
|
||||||
apiKey: "PROXY_API_KEY", // env var name or literal
|
apiKey: "PROXY_API_KEY", // env var name or literal
|
||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
@@ -1561,6 +1562,7 @@ pi.registerProvider("corporate-ai", {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Config options:**
|
**Config options:**
|
||||||
|
- `name` - Display name for the provider in UI such as `/login`.
|
||||||
- `baseUrl` - API endpoint URL. Required when defining models.
|
- `baseUrl` - API endpoint URL. Required when defining models.
|
||||||
- `apiKey` - API key or environment variable name. Required when defining models (unless `oauth` provided).
|
- `apiKey` - API key or environment variable name. Required when defining models (unless `oauth` provided).
|
||||||
- `api` - API type: `"anthropic-messages"`, `"openai-completions"`, `"openai-responses"`, etc.
|
- `api` - API type: `"anthropic-messages"`, `"openai-completions"`, `"openai-responses"`, etc.
|
||||||
|
|||||||
@@ -1302,6 +1302,8 @@ export interface ExtensionAPI {
|
|||||||
|
|
||||||
/** Configuration for registering a provider via pi.registerProvider(). */
|
/** Configuration for registering a provider via pi.registerProvider(). */
|
||||||
export interface ProviderConfig {
|
export interface ProviderConfig {
|
||||||
|
/** Display name for the provider in UI. */
|
||||||
|
name?: string;
|
||||||
/** Base URL for the API endpoint. Required when defining models. */
|
/** Base URL for the API endpoint. Required when defining models. */
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
/** API key or environment variable name. Required when defining models (unless oauth provided). */
|
/** API key or environment variable name. Required when defining models (unless oauth provided). */
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { Compile } from "typebox/compile";
|
|||||||
import type { TLocalizedValidationError } from "typebox/error";
|
import type { TLocalizedValidationError } from "typebox/error";
|
||||||
import { getAgentDir } from "../config.js";
|
import { getAgentDir } from "../config.js";
|
||||||
import type { AuthStatus, AuthStorage } from "./auth-storage.js";
|
import type { AuthStatus, AuthStorage } from "./auth-storage.js";
|
||||||
|
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.js";
|
||||||
import {
|
import {
|
||||||
clearConfigValueCache,
|
clearConfigValueCache,
|
||||||
resolveConfigValueOrThrow,
|
resolveConfigValueOrThrow,
|
||||||
@@ -177,6 +178,7 @@ const ModelOverrideSchema = Type.Object({
|
|||||||
type ModelOverride = Static<typeof ModelOverrideSchema>;
|
type ModelOverride = Static<typeof ModelOverrideSchema>;
|
||||||
|
|
||||||
const ProviderConfigSchema = Type.Object({
|
const ProviderConfigSchema = Type.Object({
|
||||||
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
apiKey: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
api: Type.Optional(Type.String({ minLength: 1 })),
|
api: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
@@ -727,6 +729,22 @@ export class ModelRegistry {
|
|||||||
return { configured: true, source: "models_json_key" };
|
return { configured: true, source: "models_json_key" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name for a provider.
|
||||||
|
*/
|
||||||
|
getProviderDisplayName(provider: string): string {
|
||||||
|
const registeredProvider = this.registeredProviders.get(provider);
|
||||||
|
const oauthProvider = this.authStorage.getOAuthProviders().find((p) => p.id === provider);
|
||||||
|
|
||||||
|
return (
|
||||||
|
registeredProvider?.name ??
|
||||||
|
registeredProvider?.oauth?.name ??
|
||||||
|
oauthProvider?.name ??
|
||||||
|
BUILT_IN_PROVIDER_DISPLAY_NAMES[provider] ??
|
||||||
|
provider
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get API key for a provider.
|
* Get API key for a provider.
|
||||||
*/
|
*/
|
||||||
@@ -893,6 +911,7 @@ export class ModelRegistry {
|
|||||||
* Input type for registerProvider API.
|
* Input type for registerProvider API.
|
||||||
*/
|
*/
|
||||||
export interface ProviderConfigInput {
|
export interface ProviderConfigInput {
|
||||||
|
name?: string;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
api?: Api;
|
api?: Api;
|
||||||
|
|||||||
24
packages/coding-agent/src/core/provider-display-names.ts
Normal file
24
packages/coding-agent/src/core/provider-display-names.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||||
|
anthropic: "Anthropic",
|
||||||
|
"amazon-bedrock": "Amazon Bedrock",
|
||||||
|
"azure-openai-responses": "Azure OpenAI Responses",
|
||||||
|
cerebras: "Cerebras",
|
||||||
|
"cloudflare-workers-ai": "Cloudflare Workers AI",
|
||||||
|
deepseek: "DeepSeek",
|
||||||
|
fireworks: "Fireworks",
|
||||||
|
google: "Google Gemini",
|
||||||
|
"google-vertex": "Google Vertex AI",
|
||||||
|
groq: "Groq",
|
||||||
|
huggingface: "Hugging Face",
|
||||||
|
"kimi-coding": "Kimi For Coding",
|
||||||
|
mistral: "Mistral",
|
||||||
|
minimax: "MiniMax",
|
||||||
|
"minimax-cn": "MiniMax (China)",
|
||||||
|
opencode: "OpenCode Zen",
|
||||||
|
"opencode-go": "OpenCode Go",
|
||||||
|
openai: "OpenAI",
|
||||||
|
openrouter: "OpenRouter",
|
||||||
|
"vercel-ai-gateway": "Vercel AI Gateway",
|
||||||
|
xai: "xAI",
|
||||||
|
zai: "ZAI",
|
||||||
|
};
|
||||||
@@ -72,6 +72,7 @@ import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.j
|
|||||||
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
||||||
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
||||||
import { DefaultPackageManager } from "../../core/package-manager.js";
|
import { DefaultPackageManager } from "../../core/package-manager.js";
|
||||||
|
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
|
||||||
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
|
||||||
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
|
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
|
||||||
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
import { type SessionContext, SessionManager } from "../../core/session-manager.js";
|
||||||
@@ -179,32 +180,6 @@ function hasDefaultModelProvider(providerId: string): providerId is keyof typeof
|
|||||||
|
|
||||||
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
|
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
|
||||||
|
|
||||||
const API_KEY_LOGIN_PROVIDERS: Record<string, string> = {
|
|
||||||
anthropic: "Anthropic",
|
|
||||||
[BEDROCK_PROVIDER_ID]: "Amazon Bedrock",
|
|
||||||
"azure-openai-responses": "Azure OpenAI Responses",
|
|
||||||
cerebras: "Cerebras",
|
|
||||||
"cloudflare-workers-ai": "Cloudflare Workers AI",
|
|
||||||
deepseek: "DeepSeek",
|
|
||||||
fireworks: "Fireworks",
|
|
||||||
google: "Google Gemini",
|
|
||||||
"google-vertex": "Google Vertex AI",
|
|
||||||
groq: "Groq",
|
|
||||||
huggingface: "Hugging Face",
|
|
||||||
"kimi-coding": "Kimi For Coding",
|
|
||||||
mistral: "Mistral",
|
|
||||||
minimax: "MiniMax",
|
|
||||||
"minimax-cn": "MiniMax (China)",
|
|
||||||
opencode: "OpenCode Zen",
|
|
||||||
"opencode-go": "OpenCode Go",
|
|
||||||
openai: "OpenAI",
|
|
||||||
openrouter: "OpenRouter",
|
|
||||||
"vercel-ai-gateway": "Vercel AI Gateway",
|
|
||||||
xai: "xAI",
|
|
||||||
zai: "ZAI",
|
|
||||||
};
|
|
||||||
|
|
||||||
const BUILT_IN_API_KEY_LOGIN_PROVIDERS = new Set(Object.keys(API_KEY_LOGIN_PROVIDERS));
|
|
||||||
const BUILT_IN_MODEL_PROVIDERS = new Set<string>(getProviders());
|
const BUILT_IN_MODEL_PROVIDERS = new Set<string>(getProviders());
|
||||||
|
|
||||||
export function isApiKeyLoginProvider(
|
export function isApiKeyLoginProvider(
|
||||||
@@ -212,7 +187,7 @@ export function isApiKeyLoginProvider(
|
|||||||
oauthProviderIds: ReadonlySet<string>,
|
oauthProviderIds: ReadonlySet<string>,
|
||||||
builtInProviderIds: ReadonlySet<string> = BUILT_IN_MODEL_PROVIDERS,
|
builtInProviderIds: ReadonlySet<string> = BUILT_IN_MODEL_PROVIDERS,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (BUILT_IN_API_KEY_LOGIN_PROVIDERS.has(providerId)) {
|
if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (builtInProviderIds.has(providerId)) {
|
if (builtInProviderIds.has(providerId)) {
|
||||||
@@ -221,10 +196,6 @@ export function isApiKeyLoginProvider(
|
|||||||
return !oauthProviderIds.has(providerId);
|
return !oauthProviderIds.has(providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiKeyProviderDisplayName(providerId: string): string {
|
|
||||||
return API_KEY_LOGIN_PROVIDERS[providerId] ?? providerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for InteractiveMode initialization.
|
* Options for InteractiveMode initialization.
|
||||||
*/
|
*/
|
||||||
@@ -4374,7 +4345,7 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
options.push({
|
options.push({
|
||||||
id: providerId,
|
id: providerId,
|
||||||
name: getApiKeyProviderDisplayName(providerId),
|
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
||||||
authType: "api_key",
|
authType: "api_key",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4385,7 +4356,6 @@ export class InteractiveMode {
|
|||||||
|
|
||||||
private getLogoutProviderOptions(): AuthSelectorProvider[] {
|
private getLogoutProviderOptions(): AuthSelectorProvider[] {
|
||||||
const authStorage = this.session.modelRegistry.authStorage;
|
const authStorage = this.session.modelRegistry.authStorage;
|
||||||
const oauthNameById = new Map(authStorage.getOAuthProviders().map((provider) => [provider.id, provider.name]));
|
|
||||||
const options: AuthSelectorProvider[] = [];
|
const options: AuthSelectorProvider[] = [];
|
||||||
|
|
||||||
for (const providerId of authStorage.list()) {
|
for (const providerId of authStorage.list()) {
|
||||||
@@ -4395,10 +4365,7 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
options.push({
|
options.push({
|
||||||
id: providerId,
|
id: providerId,
|
||||||
name:
|
name: this.session.modelRegistry.getProviderDisplayName(providerId),
|
||||||
credential.type === "oauth"
|
|
||||||
? (oauthNameById.get(providerId) ?? providerId)
|
|
||||||
: getApiKeyProviderDisplayName(providerId),
|
|
||||||
authType: credential.type,
|
authType: credential.type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -841,6 +841,56 @@ describe("ModelRegistry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("dynamic provider lifecycle", () => {
|
describe("dynamic provider lifecycle", () => {
|
||||||
|
test("getProviderDisplayName resolves registered, OAuth, built-in, and fallback names", () => {
|
||||||
|
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
|
||||||
|
|
||||||
|
expect(registry.getProviderDisplayName("openai")).toBe("OpenAI");
|
||||||
|
expect(registry.getProviderDisplayName("github-copilot")).toBe("GitHub Copilot");
|
||||||
|
expect(registry.getProviderDisplayName("unknown-provider")).toBe("unknown-provider");
|
||||||
|
|
||||||
|
registry.registerProvider("named-provider", {
|
||||||
|
name: "Named Provider",
|
||||||
|
baseUrl: "https://provider.test/v1",
|
||||||
|
apiKey: "TEST_KEY",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "demo-model",
|
||||||
|
name: "Demo Model",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 4096,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(registry.getProviderDisplayName("named-provider")).toBe("Named Provider");
|
||||||
|
|
||||||
|
registry.registerProvider("oauth-provider", {
|
||||||
|
baseUrl: "https://provider.test/v1",
|
||||||
|
api: "openai-completions",
|
||||||
|
oauth: {
|
||||||
|
name: "OAuth Provider",
|
||||||
|
login: async () => ({ access: "access", refresh: "refresh", expires: Date.now() + 60_000 }),
|
||||||
|
refreshToken: async (credentials) => credentials,
|
||||||
|
getApiKey: (credentials) => credentials.access,
|
||||||
|
},
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "demo-model",
|
||||||
|
name: "Demo Model",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 4096,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(registry.getProviderDisplayName("oauth-provider")).toBe("OAuth Provider");
|
||||||
|
});
|
||||||
|
|
||||||
test("failed registerProvider does not persist invalid streamSimple config", () => {
|
test("failed registerProvider does not persist invalid streamSimple config", () => {
|
||||||
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
|
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import stripAnsi from "strip-ansi";
|
|||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||||
import { KeybindingsManager } from "../src/core/keybindings.js";
|
import { KeybindingsManager } from "../src/core/keybindings.js";
|
||||||
|
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../src/core/provider-display-names.js";
|
||||||
import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.js";
|
import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.js";
|
||||||
import { getApiKeyProviderDisplayName, isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.js";
|
import { isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.js";
|
||||||
import { initTheme } from "../src/modes/interactive/theme/theme.js";
|
import { initTheme } from "../src/modes/interactive/theme/theme.js";
|
||||||
|
|
||||||
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
||||||
@@ -31,7 +32,7 @@ describe("OAuthSelectorComponent", () => {
|
|||||||
const builtInProviderIds = new Set(["anthropic", "github-copilot", "amazon-bedrock", "openai"]);
|
const builtInProviderIds = new Set(["anthropic", "github-copilot", "amazon-bedrock", "openai"]);
|
||||||
|
|
||||||
expect(isApiKeyLoginProvider("anthropic", oauthProviderIds, builtInProviderIds)).toBe(true);
|
expect(isApiKeyLoginProvider("anthropic", oauthProviderIds, builtInProviderIds)).toBe(true);
|
||||||
expect(getApiKeyProviderDisplayName("anthropic")).toBe("Anthropic");
|
expect(BUILT_IN_PROVIDER_DISPLAY_NAMES.anthropic).toBe("Anthropic");
|
||||||
expect(isApiKeyLoginProvider("openai", oauthProviderIds, builtInProviderIds)).toBe(true);
|
expect(isApiKeyLoginProvider("openai", oauthProviderIds, builtInProviderIds)).toBe(true);
|
||||||
expect(isApiKeyLoginProvider("github-copilot", oauthProviderIds, builtInProviderIds)).toBe(false);
|
expect(isApiKeyLoginProvider("github-copilot", oauthProviderIds, builtInProviderIds)).toBe(false);
|
||||||
expect(isApiKeyLoginProvider("amazon-bedrock", oauthProviderIds, builtInProviderIds)).toBe(true);
|
expect(isApiKeyLoginProvider("amazon-bedrock", oauthProviderIds, builtInProviderIds)).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user