mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
Merge branch 'pr-1669-fixes'
# Conflicts: # package-lock.json # packages/ai/CHANGELOG.md # packages/coding-agent/CHANGELOG.md
This commit is contained in:
commit
3dcb3c1c77
14 changed files with 828 additions and 440 deletions
909
package-lock.json
generated
909
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -50,6 +50,7 @@
|
|||
},
|
||||
"overrides": {
|
||||
"rimraf": "6.1.2",
|
||||
"fast-xml-parser": "5.3.8",
|
||||
"gaxios": {
|
||||
"rimraf": "6.1.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Restored built-in OAuth providers when unregistering dynamically registered provider IDs and added `resetOAuthProviders()` for registry reset flows.
|
||||
|
||||
## [0.55.1] - 2026-02-26
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -42,13 +42,17 @@ import { geminiCliOAuthProvider } from "./google-gemini-cli.js";
|
|||
import { openaiCodexOAuthProvider } from "./openai-codex.js";
|
||||
import type { OAuthCredentials, OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface } from "./types.js";
|
||||
|
||||
const oauthProviderRegistry = new Map<string, OAuthProviderInterface>([
|
||||
[anthropicOAuthProvider.id, anthropicOAuthProvider],
|
||||
[githubCopilotOAuthProvider.id, githubCopilotOAuthProvider],
|
||||
[geminiCliOAuthProvider.id, geminiCliOAuthProvider],
|
||||
[antigravityOAuthProvider.id, antigravityOAuthProvider],
|
||||
[openaiCodexOAuthProvider.id, openaiCodexOAuthProvider],
|
||||
]);
|
||||
const BUILT_IN_OAUTH_PROVIDERS: OAuthProviderInterface[] = [
|
||||
anthropicOAuthProvider,
|
||||
githubCopilotOAuthProvider,
|
||||
geminiCliOAuthProvider,
|
||||
antigravityOAuthProvider,
|
||||
openaiCodexOAuthProvider,
|
||||
];
|
||||
|
||||
const oauthProviderRegistry = new Map<string, OAuthProviderInterface>(
|
||||
BUILT_IN_OAUTH_PROVIDERS.map((provider) => [provider.id, provider]),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get an OAuth provider by ID
|
||||
|
|
@ -64,6 +68,31 @@ export function registerOAuthProvider(provider: OAuthProviderInterface): void {
|
|||
oauthProviderRegistry.set(provider.id, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an OAuth provider.
|
||||
*
|
||||
* If the provider is built-in, restores the built-in implementation.
|
||||
* Custom providers are removed completely.
|
||||
*/
|
||||
export function unregisterOAuthProvider(id: string): void {
|
||||
const builtInProvider = BUILT_IN_OAUTH_PROVIDERS.find((provider) => provider.id === id);
|
||||
if (builtInProvider) {
|
||||
oauthProviderRegistry.set(id, builtInProvider);
|
||||
return;
|
||||
}
|
||||
oauthProviderRegistry.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset OAuth providers to built-ins.
|
||||
*/
|
||||
export function resetOAuthProviders(): void {
|
||||
oauthProviderRegistry.clear();
|
||||
for (const provider of BUILT_IN_OAUTH_PROVIDERS) {
|
||||
oauthProviderRegistry.set(provider.id, provider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered OAuth providers
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- `pi.unregisterProvider(name)` removes a dynamically registered provider and its models from the registry without requiring `/reload`. Built-in models that were overridden by the provider are restored ([#1669](https://github.com/badlogic/pi-mono/pull/1669) by [@aliou](https://github.com/aliou)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- `pi.registerProvider()` now takes effect immediately when called after the initial extension load phase (e.g. from a command handler). Previously the registration sat in a pending queue that was never flushed until the next `/reload` ([#1669](https://github.com/badlogic/pi-mono/pull/1669) by [@aliou](https://github.com/aliou)).
|
||||
|
||||
## [0.55.1] - 2026-02-26
|
||||
|
||||
### New Features
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ See these complete provider examples:
|
|||
- [Quick Reference](#quick-reference)
|
||||
- [Override Existing Provider](#override-existing-provider)
|
||||
- [Register New Provider](#register-new-provider)
|
||||
- [Unregister Provider](#unregister-provider)
|
||||
- [OAuth Support](#oauth-support)
|
||||
- [Custom Streaming API](#custom-streaming-api)
|
||||
- [Testing Your Implementation](#testing-your-implementation)
|
||||
|
|
@ -116,6 +117,37 @@ pi.registerProvider("my-llm", {
|
|||
|
||||
When `models` is provided, it **replaces** all existing models for that provider.
|
||||
|
||||
## Unregister Provider
|
||||
|
||||
Use `pi.unregisterProvider(name)` to remove a provider that was previously registered via `pi.registerProvider(name, ...)`:
|
||||
|
||||
```typescript
|
||||
// Register
|
||||
pi.registerProvider("my-llm", {
|
||||
baseUrl: "https://api.my-llm.com/v1",
|
||||
apiKey: "MY_LLM_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "my-llm-large",
|
||||
name: "My LLM Large",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 16384
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Later, remove it
|
||||
pi.unregisterProvider("my-llm");
|
||||
```
|
||||
|
||||
Unregistering removes that provider's dynamic models, API key fallback, OAuth provider registration, and custom stream handler registrations. Any built-in models or provider behavior that were overridden are restored.
|
||||
|
||||
Calls made after the initial extension load phase are applied immediately, so no `/reload` is required.
|
||||
|
||||
### API Types
|
||||
|
||||
The `api` field determines which streaming implementation is used:
|
||||
|
|
|
|||
|
|
@ -1161,6 +1161,8 @@ pi.events.emit("my:event", { ... });
|
|||
|
||||
Register or override a model provider dynamically. Useful for proxies, custom endpoints, or team-wide model configurations.
|
||||
|
||||
Calls made during the extension factory function are queued and applied once the runner initialises. Calls made after that — for example from a command handler following a user setup flow — take effect immediately without requiring a `/reload`.
|
||||
|
||||
```typescript
|
||||
// Register a new provider with custom models
|
||||
pi.registerProvider("my-proxy", {
|
||||
|
|
@ -1221,6 +1223,21 @@ pi.registerProvider("corporate-ai", {
|
|||
|
||||
See [custom-provider.md](custom-provider.md) for advanced topics: custom streaming APIs, OAuth details, model definition reference.
|
||||
|
||||
### pi.unregisterProvider(name)
|
||||
|
||||
Remove a previously registered provider and its models. Built-in models that were overridden by the provider are restored. Has no effect if the provider was not registered.
|
||||
|
||||
Like `registerProvider`, this takes effect immediately when called after the initial load phase, so a `/reload` is not required.
|
||||
|
||||
```typescript
|
||||
pi.registerCommand("my-setup-teardown", {
|
||||
description: "Remove the custom proxy provider",
|
||||
handler: async (_args, _ctx) => {
|
||||
pi.unregisterProvider("my-proxy");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
Extensions with state should store it in tool result `details` for proper branching support:
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
"hosted-git-info": "^9.0.2",
|
||||
"ignore": "^7.0.5",
|
||||
"marked": "^15.0.12",
|
||||
"minimatch": "^10.1.1",
|
||||
"minimatch": "^10.2.3",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export function createExtensionRuntime(): ExtensionRuntime {
|
|||
throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
|
||||
};
|
||||
|
||||
return {
|
||||
const runtime: ExtensionRuntime = {
|
||||
sendMessage: notInitialized,
|
||||
sendUserMessage: notInitialized,
|
||||
appendEntry: notInitialized,
|
||||
|
|
@ -125,7 +125,17 @@ export function createExtensionRuntime(): ExtensionRuntime {
|
|||
setThinkingLevel: notInitialized,
|
||||
flagValues: new Map(),
|
||||
pendingProviderRegistrations: [],
|
||||
// Pre-bind: queue registrations so bindCore() can flush them once the
|
||||
// model registry is available. bindCore() replaces both with direct calls.
|
||||
registerProvider: (name, config) => {
|
||||
runtime.pendingProviderRegistrations.push({ name, config });
|
||||
},
|
||||
unregisterProvider: (name) => {
|
||||
runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);
|
||||
},
|
||||
};
|
||||
|
||||
return runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -246,7 +256,11 @@ function createExtensionAPI(
|
|||
},
|
||||
|
||||
registerProvider(name: string, config: ProviderConfig) {
|
||||
runtime.pendingProviderRegistrations.push({ name, config });
|
||||
runtime.registerProvider(name, config);
|
||||
},
|
||||
|
||||
unregisterProvider(name: string) {
|
||||
runtime.unregisterProvider(name);
|
||||
},
|
||||
|
||||
events: eventBus,
|
||||
|
|
|
|||
|
|
@ -259,11 +259,16 @@ export class ExtensionRunner {
|
|||
this.compactFn = contextActions.compact;
|
||||
this.getSystemPromptFn = contextActions.getSystemPrompt;
|
||||
|
||||
// Process provider registrations queued during extension loading
|
||||
// Flush provider registrations queued during extension loading
|
||||
for (const { name, config } of this.runtime.pendingProviderRegistrations) {
|
||||
this.modelRegistry.registerProvider(name, config);
|
||||
}
|
||||
this.runtime.pendingProviderRegistrations = [];
|
||||
|
||||
// From this point on, provider registration/unregistration takes effect immediately
|
||||
// without requiring a /reload.
|
||||
this.runtime.registerProvider = (name, config) => this.modelRegistry.registerProvider(name, config);
|
||||
this.runtime.unregisterProvider = (name) => this.modelRegistry.unregisterProvider(name);
|
||||
}
|
||||
|
||||
bindCommandContext(actions?: ExtensionCommandContextActions): void {
|
||||
|
|
|
|||
|
|
@ -1071,6 +1071,11 @@ export interface ExtensionAPI {
|
|||
* If `oauth` is provided: registers OAuth provider for /login support.
|
||||
* If `streamSimple` is provided: registers a custom API stream handler.
|
||||
*
|
||||
* During initial extension load this call is queued and applied once the
|
||||
* runner has bound its context. After that it takes effect immediately, so
|
||||
* it is safe to call from command handlers or event callbacks without
|
||||
* requiring a `/reload`.
|
||||
*
|
||||
* @example
|
||||
* // Register a new provider with custom models
|
||||
* pi.registerProvider("my-proxy", {
|
||||
|
|
@ -1112,6 +1117,21 @@ export interface ExtensionAPI {
|
|||
*/
|
||||
registerProvider(name: string, config: ProviderConfig): void;
|
||||
|
||||
/**
|
||||
* Unregister a previously registered provider.
|
||||
*
|
||||
* Removes all models belonging to the named provider and restores any
|
||||
* built-in models that were overridden by it. Has no effect if the provider
|
||||
* is not currently registered.
|
||||
*
|
||||
* Like `registerProvider`, this takes effect immediately when called after
|
||||
* the initial load phase.
|
||||
*
|
||||
* @example
|
||||
* pi.unregisterProvider("my-proxy");
|
||||
*/
|
||||
unregisterProvider(name: string): void;
|
||||
|
||||
/** Shared event bus for extension communication. */
|
||||
events: EventBus;
|
||||
}
|
||||
|
|
@ -1247,6 +1267,14 @@ export interface ExtensionRuntimeState {
|
|||
flagValues: Map<string, boolean | string>;
|
||||
/** Provider registrations queued during extension loading, processed when runner binds */
|
||||
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig }>;
|
||||
/**
|
||||
* Register or unregister a provider.
|
||||
*
|
||||
* Before bindCore(): queues registrations / removes from queue.
|
||||
* After bindCore(): calls ModelRegistry directly for immediate effect.
|
||||
*/
|
||||
registerProvider: (name: string, config: ProviderConfig) => void;
|
||||
unregisterProvider: (name: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import {
|
|||
type OpenAIResponsesCompat,
|
||||
registerApiProvider,
|
||||
registerOAuthProvider,
|
||||
resetApiProviders,
|
||||
resetOAuthProviders,
|
||||
type SimpleStreamOptions,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
|
|
@ -243,6 +245,11 @@ export class ModelRegistry {
|
|||
refresh(): void {
|
||||
this.customProviderApiKeys.clear();
|
||||
this.loadError = undefined;
|
||||
|
||||
// Ensure dynamic API/OAuth registrations are rebuilt from current provider state.
|
||||
resetApiProviders();
|
||||
resetOAuthProviders();
|
||||
|
||||
this.loadModels();
|
||||
|
||||
for (const [providerName, config] of this.registeredProviders.entries()) {
|
||||
|
|
@ -540,6 +547,22 @@ export class ModelRegistry {
|
|||
this.applyProviderConfig(providerName, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a previously registered provider.
|
||||
*
|
||||
* Removes the provider from the registry and reloads models from disk so that
|
||||
* built-in models overridden by this provider are restored to their original state.
|
||||
* Also resets dynamic OAuth and API stream registrations before reapplying
|
||||
* remaining dynamic providers.
|
||||
* Has no effect if the provider was never registered.
|
||||
*/
|
||||
unregisterProvider(providerName: string): void {
|
||||
if (!this.registeredProviders.has(providerName)) return;
|
||||
this.registeredProviders.delete(providerName);
|
||||
this.customProviderApiKeys.delete(providerName);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private applyProviderConfig(providerName: string, config: ProviderConfigInput): void {
|
||||
// Register OAuth provider if provided
|
||||
if (config.oauth) {
|
||||
|
|
@ -556,11 +579,14 @@ export class ModelRegistry {
|
|||
throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`);
|
||||
}
|
||||
const streamSimple = config.streamSimple;
|
||||
registerApiProvider({
|
||||
api: config.api,
|
||||
stream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),
|
||||
streamSimple,
|
||||
});
|
||||
registerApiProvider(
|
||||
{
|
||||
api: config.api,
|
||||
stream: (model, context, options) => streamSimple(model, context, options as SimpleStreamOptions),
|
||||
streamSimple,
|
||||
},
|
||||
`provider:${providerName}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Store API key for auth resolution
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import * as os from "node:os";
|
|||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
|
||||
import { createExtensionRuntime, discoverAndLoadExtensions } from "../src/core/extensions/loader.js";
|
||||
import { ExtensionRunner } from "../src/core/extensions/runner.js";
|
||||
import type { ExtensionActions, ExtensionContextActions, ProviderConfig } from "../src/core/extensions/types.js";
|
||||
import { DEFAULT_KEYBINDINGS, type KeyId } from "../src/core/keybindings.js";
|
||||
import { ModelRegistry } from "../src/core/model-registry.js";
|
||||
import { SessionManager } from "../src/core/session-manager.js";
|
||||
|
|
@ -32,6 +33,50 @@ describe("ExtensionRunner", () => {
|
|||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const providerModelConfig: ProviderConfig = {
|
||||
baseUrl: "https://provider.test/v1",
|
||||
apiKey: "PROVIDER_TEST_KEY",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "instant-model",
|
||||
name: "Instant Model",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const extensionActions: ExtensionActions = {
|
||||
sendMessage: () => {},
|
||||
sendUserMessage: () => {},
|
||||
appendEntry: () => {},
|
||||
setSessionName: () => {},
|
||||
getSessionName: () => undefined,
|
||||
setLabel: () => {},
|
||||
getActiveTools: () => [],
|
||||
getAllTools: () => [],
|
||||
setActiveTools: () => {},
|
||||
getCommands: () => [],
|
||||
setModel: async () => false,
|
||||
getThinkingLevel: () => "off",
|
||||
setThinkingLevel: () => {},
|
||||
};
|
||||
|
||||
const extensionContextActions: ExtensionContextActions = {
|
||||
getModel: () => undefined,
|
||||
isIdle: () => true,
|
||||
abort: () => {},
|
||||
hasPendingMessages: () => false,
|
||||
shutdown: () => {},
|
||||
getContextUsage: () => undefined,
|
||||
compact: () => {},
|
||||
getSystemPrompt: () => "",
|
||||
};
|
||||
|
||||
describe("shortcut conflicts", () => {
|
||||
it("warns when extension shortcut conflicts with built-in", async () => {
|
||||
const extCode = `
|
||||
|
|
@ -557,6 +602,47 @@ describe("ExtensionRunner", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("provider registration", () => {
|
||||
it("pre-bind unregister removes all queued registrations for a provider", () => {
|
||||
const runtime = createExtensionRuntime();
|
||||
|
||||
runtime.registerProvider("queued-provider", providerModelConfig);
|
||||
runtime.registerProvider("queued-provider", {
|
||||
...providerModelConfig,
|
||||
models: [
|
||||
{
|
||||
id: "instant-model-2",
|
||||
name: "Instant Model 2",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(2);
|
||||
|
||||
runtime.unregisterProvider("queued-provider");
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("post-bind register and unregister take effect immediately", () => {
|
||||
const runtime = createExtensionRuntime();
|
||||
const runner = new ExtensionRunner([], runtime, tempDir, sessionManager, modelRegistry);
|
||||
|
||||
runner.bindCore(extensionActions, extensionContextActions);
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(0);
|
||||
|
||||
runtime.registerProvider("instant-provider", providerModelConfig);
|
||||
expect(runtime.pendingProviderRegistrations).toHaveLength(0);
|
||||
expect(modelRegistry.find("instant-provider", "instant-model")).toBeDefined();
|
||||
|
||||
runtime.unregisterProvider("instant-provider");
|
||||
expect(modelRegistry.find("instant-provider", "instant-model")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasHandlers", () => {
|
||||
it("returns true when handlers exist for event type", async () => {
|
||||
const extCode = `
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { OpenAICompletionsCompat } from "@mariozechner/pi-ai";
|
||||
import type { Api, Context, Model, OpenAICompletionsCompat } from "@mariozechner/pi-ai";
|
||||
import { getApiProvider, getOAuthProvider } from "@mariozechner/pi-ai";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { AuthStorage } from "../src/core/auth-storage.js";
|
||||
import { clearApiKeyCache, ModelRegistry } from "../src/core/model-registry.js";
|
||||
|
|
@ -65,6 +66,23 @@ describe("ModelRegistry", () => {
|
|||
writeFileSync(modelsJsonPath, JSON.stringify({ providers }));
|
||||
}
|
||||
|
||||
const openAiModel: Model<Api> = {
|
||||
id: "test-openai-model",
|
||||
name: "Test OpenAI Model",
|
||||
api: "openai-completions",
|
||||
provider: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
};
|
||||
|
||||
const emptyContext: Context = {
|
||||
messages: [],
|
||||
};
|
||||
|
||||
describe("baseUrl override (no custom models)", () => {
|
||||
test("overriding baseUrl keeps all built-in models", () => {
|
||||
writeRawModelsJson({
|
||||
|
|
@ -527,6 +545,61 @@ describe("ModelRegistry", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("dynamic provider lifecycle", () => {
|
||||
test("unregisterProvider removes custom OAuth provider and restores built-in OAuth provider", () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
|
||||
registry.registerProvider("anthropic", {
|
||||
oauth: {
|
||||
name: "Custom Anthropic OAuth",
|
||||
login: async () => ({
|
||||
access: "custom-access-token",
|
||||
refresh: "custom-refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
}),
|
||||
refreshToken: async (credentials) => credentials,
|
||||
getApiKey: (credentials) => credentials.access,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getOAuthProvider("anthropic")?.name).toBe("Custom Anthropic OAuth");
|
||||
|
||||
registry.unregisterProvider("anthropic");
|
||||
|
||||
expect(getOAuthProvider("anthropic")?.name).not.toBe("Custom Anthropic OAuth");
|
||||
});
|
||||
|
||||
test("unregisterProvider removes custom streamSimple override and restores built-in API stream handler", () => {
|
||||
const registry = new ModelRegistry(authStorage, modelsJsonPath);
|
||||
|
||||
registry.registerProvider("stream-override-provider", {
|
||||
api: "openai-completions",
|
||||
streamSimple: () => {
|
||||
throw new Error("custom streamSimple override");
|
||||
},
|
||||
});
|
||||
|
||||
let threwCustomOverride = false;
|
||||
try {
|
||||
getApiProvider("openai-completions")?.streamSimple(openAiModel, emptyContext);
|
||||
} catch (error) {
|
||||
threwCustomOverride = error instanceof Error && error.message === "custom streamSimple override";
|
||||
}
|
||||
expect(threwCustomOverride).toBe(true);
|
||||
|
||||
registry.unregisterProvider("stream-override-provider");
|
||||
|
||||
let threwCustomOverrideAfterUnregister = false;
|
||||
try {
|
||||
getApiProvider("openai-completions")?.streamSimple(openAiModel, emptyContext);
|
||||
} catch (error) {
|
||||
threwCustomOverrideAfterUnregister =
|
||||
error instanceof Error && error.message === "custom streamSimple override";
|
||||
}
|
||||
expect(threwCustomOverrideAfterUnregister).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("API key resolution", () => {
|
||||
/** Create provider config with custom apiKey */
|
||||
function providerWithApiKey(apiKey: string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue