@animus-labs/cortex 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- package/src/working-tags.ts +118 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProviderManager: standalone class wrapping pi-ai for provider discovery,
|
|
3
|
+
* OAuth login/refresh, API key validation, model resolution, and custom
|
|
4
|
+
* endpoint creation.
|
|
5
|
+
*
|
|
6
|
+
* ProviderManager and CortexAgent are fully independent. Neither knows
|
|
7
|
+
* about the other. The consumer creates both, uses ProviderManager for
|
|
8
|
+
* auth/discovery, and provides a getApiKey callback to CortexAgent.
|
|
9
|
+
*
|
|
10
|
+
* Pi-ai is loaded dynamically so consumers never import it directly.
|
|
11
|
+
* If the dependency is missing or unavailable, methods that require it
|
|
12
|
+
* throw clear errors.
|
|
13
|
+
*
|
|
14
|
+
* Reference: provider-manager.md
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
PROVIDER_REGISTRY,
|
|
19
|
+
OAUTH_PROVIDER_IDS,
|
|
20
|
+
UTILITY_MODEL_DEFAULTS,
|
|
21
|
+
} from './provider-registry.js';
|
|
22
|
+
import type { ThinkingLevel } from './types.js';
|
|
23
|
+
import type { ProviderInfo, ModelInfo } from './provider-registry.js';
|
|
24
|
+
import { wrapModel } from './model-wrapper.js';
|
|
25
|
+
import type { CortexModel } from './model-wrapper.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// OAuth types
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Callbacks provided by the consumer during an OAuth flow. */
|
|
32
|
+
export interface OAuthCallbacks {
|
|
33
|
+
/**
|
|
34
|
+
* Called when the user needs to visit a URL to authorize.
|
|
35
|
+
* The consumer should open the URL in a browser or display it.
|
|
36
|
+
*/
|
|
37
|
+
onAuth: (info: { url: string; instructions?: string }) => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Called when the OAuth flow needs user input (e.g., a prompt).
|
|
41
|
+
* The consumer should display the prompt and return the user's response.
|
|
42
|
+
*/
|
|
43
|
+
onPrompt: (prompt: { message: string; placeholder?: string; allowEmpty?: boolean }) => Promise<string>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Called with progress messages during the flow.
|
|
47
|
+
*/
|
|
48
|
+
onProgress?: (message: string) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Called when a callback-server OAuth flow needs the user to paste a
|
|
52
|
+
* manual authorization code.
|
|
53
|
+
*/
|
|
54
|
+
onManualCodeInput?: () => Promise<string>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Called when the OAuth flow needs the user to choose from provider-specific
|
|
58
|
+
* options, such as a Copilot organization or endpoint.
|
|
59
|
+
*/
|
|
60
|
+
onSelect?: (prompt: {
|
|
61
|
+
message: string;
|
|
62
|
+
options: Array<{ id: string; label: string }>;
|
|
63
|
+
}) => Promise<string | undefined>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Display-safe metadata extracted at login time. */
|
|
67
|
+
export interface OAuthMeta {
|
|
68
|
+
/** Provider identifier. */
|
|
69
|
+
provider: string;
|
|
70
|
+
/** Display name, email, or account identifier (if available). */
|
|
71
|
+
displayName?: string | undefined;
|
|
72
|
+
/** When the access token expires (Unix timestamp ms). Undefined if non-expiring. */
|
|
73
|
+
expiresAt?: number | undefined;
|
|
74
|
+
/** Whether the credential supports automatic refresh. */
|
|
75
|
+
refreshable: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Result of a successful OAuth login. */
|
|
79
|
+
export interface OAuthResult {
|
|
80
|
+
/**
|
|
81
|
+
* Serialized credential payload. The consumer stores this (encrypted)
|
|
82
|
+
* and passes it back to resolveOAuthApiKey() for refresh.
|
|
83
|
+
* Treat as an opaque blob: encrypt, store, decrypt, pass back. Never parse.
|
|
84
|
+
*/
|
|
85
|
+
credentials: string;
|
|
86
|
+
/** Display-safe metadata extracted at login time. */
|
|
87
|
+
meta: OAuthMeta;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Result of resolving/refreshing an OAuth API key. */
|
|
91
|
+
export interface OAuthRefreshResult {
|
|
92
|
+
/** The API key to use for LLM calls. */
|
|
93
|
+
apiKey: string;
|
|
94
|
+
/**
|
|
95
|
+
* Credential payload (may be updated if refresh occurred).
|
|
96
|
+
* Same format as OAuthResult.credentials.
|
|
97
|
+
*/
|
|
98
|
+
credentials: string;
|
|
99
|
+
/** Updated metadata. */
|
|
100
|
+
meta: OAuthMeta;
|
|
101
|
+
/** Whether the credentials were actually refreshed (true) or reused as-is (false). */
|
|
102
|
+
changed: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Configuration for creating a custom model endpoint. */
|
|
106
|
+
export interface CustomModelConfig {
|
|
107
|
+
/** Base URL of the OpenAI-compatible API (e.g., 'http://localhost:11434/v1'). */
|
|
108
|
+
baseUrl: string;
|
|
109
|
+
/** Model identifier to send in API requests. */
|
|
110
|
+
modelId: string;
|
|
111
|
+
/** Context window size (default: 128,000). */
|
|
112
|
+
contextWindow?: number | undefined;
|
|
113
|
+
/** Optional API key (some local servers don't require one). */
|
|
114
|
+
apiKey?: string | undefined;
|
|
115
|
+
/** Compatibility settings for non-standard servers. */
|
|
116
|
+
compat?: {
|
|
117
|
+
/** Whether the server supports the 'developer' role (default: true). */
|
|
118
|
+
supportsDeveloperRole?: boolean | undefined;
|
|
119
|
+
/** Whether the server supports reasoning_effort (default: true). */
|
|
120
|
+
supportsReasoningEffort?: boolean | undefined;
|
|
121
|
+
} | undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type ApiKeyValidationStatus =
|
|
125
|
+
| 'valid'
|
|
126
|
+
| 'invalid_credentials'
|
|
127
|
+
| 'transient_error'
|
|
128
|
+
| 'resolution_error';
|
|
129
|
+
|
|
130
|
+
export interface ApiKeyValidationResult {
|
|
131
|
+
provider: string;
|
|
132
|
+
modelId: string | null;
|
|
133
|
+
valid: boolean;
|
|
134
|
+
retryable: boolean;
|
|
135
|
+
status: ApiKeyValidationStatus;
|
|
136
|
+
message?: string | undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// IProviderManager interface
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Interface for provider management operations.
|
|
145
|
+
* Consumers can mock this for testing.
|
|
146
|
+
*/
|
|
147
|
+
export interface IProviderManager {
|
|
148
|
+
// Discovery
|
|
149
|
+
listProviders(): ProviderInfo[];
|
|
150
|
+
listOAuthProviders(): string[];
|
|
151
|
+
listModels(provider: string): Promise<ModelInfo[]>;
|
|
152
|
+
|
|
153
|
+
// OAuth
|
|
154
|
+
initiateOAuth(provider: string, callbacks: OAuthCallbacks): Promise<OAuthResult>;
|
|
155
|
+
cancelOAuth(): void;
|
|
156
|
+
resolveOAuthApiKey(provider: string, credentials: string): Promise<OAuthRefreshResult>;
|
|
157
|
+
|
|
158
|
+
// API Key
|
|
159
|
+
validateApiKey(provider: string, apiKey: string): Promise<ApiKeyValidationResult>;
|
|
160
|
+
checkEnvApiKey(provider: string): string | null;
|
|
161
|
+
|
|
162
|
+
// Model Resolution
|
|
163
|
+
resolveModel(provider: string, modelId: string): Promise<CortexModel>;
|
|
164
|
+
createCustomModel(config: CustomModelConfig): Promise<CortexModel>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Pi-ai dynamic import types
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/** Shape of the pi-ai main module functions we use. */
|
|
172
|
+
interface PiAiModule {
|
|
173
|
+
getModel: (provider: string, modelId: string) => unknown;
|
|
174
|
+
getModels: (provider: string) => Array<Record<string, unknown>>;
|
|
175
|
+
getEnvApiKey: (provider: string) => string | undefined;
|
|
176
|
+
getSupportedThinkingLevels?: ((model: unknown) => string[]) | undefined;
|
|
177
|
+
completeSimple?: ((model: unknown, context: unknown, options?: unknown) => Promise<unknown>) | undefined;
|
|
178
|
+
complete?: ((model: unknown, context: unknown, options?: unknown) => Promise<unknown>) | undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface PiOAuthProvider {
|
|
182
|
+
id: string;
|
|
183
|
+
name: string;
|
|
184
|
+
login: (callbacks: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface PiAiOAuthModule {
|
|
188
|
+
getOAuthProvider?: ((id: string) => PiOAuthProvider | undefined) | undefined;
|
|
189
|
+
getOAuthProviders?: (() => PiOAuthProvider[]) | undefined;
|
|
190
|
+
getOAuthApiKey?: ((
|
|
191
|
+
provider: string,
|
|
192
|
+
credentials: Record<string, unknown>,
|
|
193
|
+
) => Promise<{ apiKey: string; newCredentials: Record<string, unknown> } | null>) | undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Pi-ai dynamic import helpers
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Lazily load the pi-ai main module.
|
|
202
|
+
* Throws a clear error if pi-ai is not installed.
|
|
203
|
+
*/
|
|
204
|
+
async function loadPiAi(): Promise<PiAiModule> {
|
|
205
|
+
try {
|
|
206
|
+
// Dynamic import with string literal to avoid bundler resolution
|
|
207
|
+
const modulePath = '@earendil-works/pi-ai';
|
|
208
|
+
return await import(/* @vite-ignore */ modulePath) as PiAiModule;
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'pi-ai is not installed. Install @earendil-works/pi-ai to use ProviderManager.'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Lazily load the pi-ai OAuth module.
|
|
218
|
+
* Throws a clear error if pi-ai is not installed.
|
|
219
|
+
*/
|
|
220
|
+
async function loadPiAiOAuth(): Promise<PiAiOAuthModule> {
|
|
221
|
+
try {
|
|
222
|
+
const modulePath = '@earendil-works/pi-ai/oauth';
|
|
223
|
+
return await import(/* @vite-ignore */ modulePath) as PiAiOAuthModule;
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error(
|
|
226
|
+
'pi-ai is not installed. Install @earendil-works/pi-ai to use OAuth features.'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Display name extraction
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Extract the best available display name from OAuth credentials.
|
|
237
|
+
* Different providers include different identity information.
|
|
238
|
+
*/
|
|
239
|
+
function extractDisplayName(credentials: Record<string, unknown>): string | undefined {
|
|
240
|
+
// Try common fields across providers
|
|
241
|
+
const email = credentials['email'];
|
|
242
|
+
if (typeof email === 'string') return email;
|
|
243
|
+
|
|
244
|
+
const accountId = credentials['accountId'];
|
|
245
|
+
if (typeof accountId === 'string') return accountId;
|
|
246
|
+
|
|
247
|
+
const idToken = credentials['idToken'];
|
|
248
|
+
if (typeof idToken === 'string') {
|
|
249
|
+
// JWT id_token may contain email in payload
|
|
250
|
+
try {
|
|
251
|
+
const parts = idToken.split('.');
|
|
252
|
+
if (parts.length >= 2) {
|
|
253
|
+
const payload = JSON.parse(atob(parts[1]!)) as Record<string, unknown>;
|
|
254
|
+
const payloadEmail = payload['email'];
|
|
255
|
+
if (typeof payloadEmail === 'string') return payloadEmail;
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// Ignore malformed tokens
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Build OAuthMeta from raw credential data.
|
|
266
|
+
*/
|
|
267
|
+
function buildOAuthMeta(
|
|
268
|
+
provider: string,
|
|
269
|
+
rawCredentials: Record<string, unknown>,
|
|
270
|
+
): OAuthMeta {
|
|
271
|
+
const displayName = extractDisplayName(rawCredentials);
|
|
272
|
+
const expiresAtRaw = rawCredentials['expiresAt'] ?? rawCredentials['expires'];
|
|
273
|
+
const expiresAt = typeof expiresAtRaw === 'number' ? expiresAtRaw : undefined;
|
|
274
|
+
|
|
275
|
+
const meta: OAuthMeta = {
|
|
276
|
+
provider,
|
|
277
|
+
refreshable: !!(rawCredentials['refreshToken'] ?? rawCredentials['refresh']),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
if (displayName !== undefined) {
|
|
281
|
+
meta.displayName = displayName;
|
|
282
|
+
}
|
|
283
|
+
if (expiresAt !== undefined) {
|
|
284
|
+
meta.expiresAt = expiresAt;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return meta;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Legacy model filtering
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Model ID prefixes considered legacy/deprecated per provider.
|
|
296
|
+
* Pi-ai doesn't flag deprecation, so we maintain this list to keep
|
|
297
|
+
* the model picker clean and prevent users from selecting models that
|
|
298
|
+
* produce poor results with modern tool-use patterns.
|
|
299
|
+
*/
|
|
300
|
+
const LEGACY_MODEL_PREFIXES: Record<string, string[]> = {
|
|
301
|
+
anthropic: [
|
|
302
|
+
'claude-3-', // Claude 3.x family (Haiku/Sonnet/Opus from 2024)
|
|
303
|
+
'claude-3.', // Alternate naming
|
|
304
|
+
],
|
|
305
|
+
openai: [
|
|
306
|
+
'gpt-3.5-', // GPT-3.5 family
|
|
307
|
+
'gpt-4-', // GPT-4 original (not 4o/4.1)
|
|
308
|
+
],
|
|
309
|
+
google: [
|
|
310
|
+
'gemini-1.', // Gemini 1.x family
|
|
311
|
+
'gemini-pro', // Original Gemini Pro
|
|
312
|
+
],
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Model mapping helper
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
const CORTEX_THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
320
|
+
'off',
|
|
321
|
+
'minimal',
|
|
322
|
+
'low',
|
|
323
|
+
'medium',
|
|
324
|
+
'high',
|
|
325
|
+
'max',
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
function mapPiThinkingLevel(level: string): ThinkingLevel | null {
|
|
329
|
+
const mapped = level === 'xhigh' ? 'max' : level;
|
|
330
|
+
return (CORTEX_THINKING_LEVELS as readonly string[]).includes(mapped)
|
|
331
|
+
? mapped as ThinkingLevel
|
|
332
|
+
: null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function mapPiThinkingLevels(levels: readonly string[]): ThinkingLevel[] {
|
|
336
|
+
const mapped: ThinkingLevel[] = [];
|
|
337
|
+
for (const level of levels) {
|
|
338
|
+
const cortexLevel = mapPiThinkingLevel(level);
|
|
339
|
+
if (cortexLevel && !mapped.includes(cortexLevel)) {
|
|
340
|
+
mapped.push(cortexLevel);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return mapped;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Map a raw pi-ai model object to our ModelInfo type.
|
|
348
|
+
*/
|
|
349
|
+
function mapRawToModelInfo(
|
|
350
|
+
raw: Record<string, unknown>,
|
|
351
|
+
getSupportedThinkingLevels?: (model: unknown) => string[],
|
|
352
|
+
): ModelInfo {
|
|
353
|
+
// pi-ai models have 'id' (API identifier like "claude-sonnet-4-6") and
|
|
354
|
+
// 'name' (display name like "Claude Sonnet 4.6"). Use 'id' as our id.
|
|
355
|
+
const rawId = raw['id'];
|
|
356
|
+
const id = typeof rawId === 'string' ? rawId : String(rawId ?? raw['name'] ?? 'unknown');
|
|
357
|
+
|
|
358
|
+
const rawDisplayName = raw['displayName'];
|
|
359
|
+
const rawName = raw['name'];
|
|
360
|
+
const name = typeof rawDisplayName === 'string'
|
|
361
|
+
? rawDisplayName
|
|
362
|
+
: typeof rawName === 'string'
|
|
363
|
+
? rawName
|
|
364
|
+
: id;
|
|
365
|
+
|
|
366
|
+
const rawContextWindow = raw['contextWindow'];
|
|
367
|
+
const contextWindow = typeof rawContextWindow === 'number' ? rawContextWindow : 200_000;
|
|
368
|
+
|
|
369
|
+
let supportedThinkingLevels: ThinkingLevel[] = [];
|
|
370
|
+
if (getSupportedThinkingLevels) {
|
|
371
|
+
try {
|
|
372
|
+
supportedThinkingLevels = mapPiThinkingLevels(getSupportedThinkingLevels(raw));
|
|
373
|
+
} catch {
|
|
374
|
+
supportedThinkingLevels = [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (supportedThinkingLevels.length === 0 && raw['reasoning'] === true) {
|
|
378
|
+
supportedThinkingLevels = ['minimal', 'low', 'medium', 'high'];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const info: ModelInfo = {
|
|
382
|
+
id,
|
|
383
|
+
name,
|
|
384
|
+
contextWindow,
|
|
385
|
+
supportsThinking: supportedThinkingLevels.some(level => level !== 'off')
|
|
386
|
+
|| !!(raw['supportsThinking'] || raw['reasoning']),
|
|
387
|
+
supportedThinkingLevels,
|
|
388
|
+
supportsImages: !!raw['supportsImages'],
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const rawPricing = raw['pricing'];
|
|
392
|
+
if (rawPricing && typeof rawPricing === 'object') {
|
|
393
|
+
const pricing = rawPricing as Record<string, unknown>;
|
|
394
|
+
const inputPrice = pricing['input'];
|
|
395
|
+
const outputPrice = pricing['output'];
|
|
396
|
+
info.pricing = {
|
|
397
|
+
input: typeof inputPrice === 'number' ? inputPrice : 0,
|
|
398
|
+
output: typeof outputPrice === 'number' ? outputPrice : 0,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return info;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// ProviderManager implementation
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
export class ProviderManager implements IProviderManager {
|
|
410
|
+
/** Active OAuth AbortController, if any. */
|
|
411
|
+
private activeOAuthAbort: AbortController | null = null;
|
|
412
|
+
|
|
413
|
+
// -----------------------------------------------------------------------
|
|
414
|
+
// Discovery
|
|
415
|
+
// -----------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* List all known providers with their metadata.
|
|
419
|
+
*/
|
|
420
|
+
listProviders(): ProviderInfo[] {
|
|
421
|
+
return PROVIDER_REGISTRY;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* List provider IDs that support OAuth authentication.
|
|
426
|
+
*/
|
|
427
|
+
listOAuthProviders(): string[] {
|
|
428
|
+
return OAUTH_PROVIDER_IDS;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* List models available from a provider.
|
|
433
|
+
* Delegates to pi-ai's getModels().
|
|
434
|
+
*
|
|
435
|
+
* @param provider - Provider identifier
|
|
436
|
+
* @returns Array of ModelInfo
|
|
437
|
+
* @throws Error if pi-ai is not installed
|
|
438
|
+
*/
|
|
439
|
+
async listModels(provider: string): Promise<ModelInfo[]> {
|
|
440
|
+
const piAi = await loadPiAi();
|
|
441
|
+
const rawModels = piAi.getModels(provider);
|
|
442
|
+
const models = rawModels.map(raw => mapRawToModelInfo(raw, piAi.getSupportedThinkingLevels));
|
|
443
|
+
|
|
444
|
+
// Filter pipeline:
|
|
445
|
+
// 1. Remove legacy/deprecated generation models
|
|
446
|
+
// 2. Remove "-latest" alias duplicates
|
|
447
|
+
// 3. Remove duplicate display names
|
|
448
|
+
const legacyPrefixes = LEGACY_MODEL_PREFIXES[provider];
|
|
449
|
+
const filtered = legacyPrefixes
|
|
450
|
+
? models.filter(m => !legacyPrefixes.some(prefix => m.id.startsWith(prefix)))
|
|
451
|
+
: models;
|
|
452
|
+
|
|
453
|
+
const seen = new Set<string>();
|
|
454
|
+
return filtered.filter(m => {
|
|
455
|
+
// Strip "-latest" suffix to check for duplicate base names
|
|
456
|
+
const baseName = m.id.replace(/-latest$/, '');
|
|
457
|
+
if (m.id.endsWith('-latest')) {
|
|
458
|
+
// Only include the "-latest" alias if no pinned version exists
|
|
459
|
+
return !filtered.some(other => other.id === baseName);
|
|
460
|
+
}
|
|
461
|
+
// Skip duplicates with identical names (different IDs but same display name)
|
|
462
|
+
if (seen.has(m.name)) return false;
|
|
463
|
+
seen.add(m.name);
|
|
464
|
+
return true;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// -----------------------------------------------------------------------
|
|
469
|
+
// OAuth
|
|
470
|
+
// -----------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Initiate an OAuth login flow for a provider.
|
|
474
|
+
*
|
|
475
|
+
* @param provider - OAuth provider identifier
|
|
476
|
+
* @param callbacks - UI callbacks for auth URL, prompts, and progress
|
|
477
|
+
* @returns The OAuth credentials and display metadata
|
|
478
|
+
* @throws Error if the provider does not support OAuth or pi-ai is not installed
|
|
479
|
+
*/
|
|
480
|
+
async initiateOAuth(provider: string, callbacks: OAuthCallbacks): Promise<OAuthResult> {
|
|
481
|
+
const oauthModule = await loadPiAiOAuth();
|
|
482
|
+
const oauthProvider = oauthModule.getOAuthProvider?.(provider);
|
|
483
|
+
if (!oauthProvider) {
|
|
484
|
+
throw new Error(`Provider "${provider}" does not support OAuth`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
this.activeOAuthAbort = new AbortController();
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const rawCredentials = await oauthProvider.login({
|
|
491
|
+
onAuth: callbacks.onAuth,
|
|
492
|
+
onPrompt: callbacks.onPrompt,
|
|
493
|
+
onProgress: callbacks.onProgress,
|
|
494
|
+
onManualCodeInput: callbacks.onManualCodeInput,
|
|
495
|
+
onSelect: callbacks.onSelect,
|
|
496
|
+
signal: this.activeOAuthAbort.signal,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
this.activeOAuthAbort = null;
|
|
500
|
+
|
|
501
|
+
const credentials = JSON.stringify(rawCredentials);
|
|
502
|
+
const meta = buildOAuthMeta(provider, rawCredentials);
|
|
503
|
+
|
|
504
|
+
return { credentials, meta };
|
|
505
|
+
} catch (err) {
|
|
506
|
+
this.activeOAuthAbort = null;
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Cancel any in-progress OAuth flow.
|
|
513
|
+
*/
|
|
514
|
+
cancelOAuth(): void {
|
|
515
|
+
if (this.activeOAuthAbort) {
|
|
516
|
+
this.activeOAuthAbort.abort();
|
|
517
|
+
this.activeOAuthAbort = null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Resolve an API key from stored OAuth credentials, refreshing if needed.
|
|
523
|
+
*
|
|
524
|
+
* @param provider - The OAuth provider
|
|
525
|
+
* @param credentials - Serialized credential blob from initiateOAuth()
|
|
526
|
+
* @returns The API key and potentially updated credentials
|
|
527
|
+
* @throws Error if pi-ai is not installed or resolution fails
|
|
528
|
+
*/
|
|
529
|
+
async resolveOAuthApiKey(provider: string, credentials: string): Promise<OAuthRefreshResult> {
|
|
530
|
+
const oauthModule = await loadPiAiOAuth();
|
|
531
|
+
const getOAuthApiKeyFn = oauthModule.getOAuthApiKey;
|
|
532
|
+
if (typeof getOAuthApiKeyFn !== 'function') {
|
|
533
|
+
throw new Error('getOAuthApiKey not found in pi-ai/oauth');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const rawCredentials = JSON.parse(credentials) as Record<string, unknown>;
|
|
537
|
+
// Security: spread rawCredentials first so Cortex-owned 'type' cannot be overridden
|
|
538
|
+
const credMap = { [provider]: { ...rawCredentials, type: 'oauth' as const } };
|
|
539
|
+
|
|
540
|
+
const result = await getOAuthApiKeyFn(provider, credMap);
|
|
541
|
+
|
|
542
|
+
if (!result) {
|
|
543
|
+
throw new Error(`OAuth resolution failed for provider "${provider}"`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const originalSerialized = credentials;
|
|
547
|
+
const newSerialized = JSON.stringify(result.newCredentials);
|
|
548
|
+
const changed = newSerialized !== originalSerialized;
|
|
549
|
+
const meta = buildOAuthMeta(provider, result.newCredentials);
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
apiKey: result.apiKey,
|
|
553
|
+
credentials: changed ? newSerialized : credentials,
|
|
554
|
+
meta,
|
|
555
|
+
changed,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// -----------------------------------------------------------------------
|
|
560
|
+
// API Key
|
|
561
|
+
// -----------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Validate an API key by making a minimal LLM call (maxTokens: 1).
|
|
565
|
+
*
|
|
566
|
+
* @param provider - The provider to validate against
|
|
567
|
+
* @param apiKey - The API key to validate
|
|
568
|
+
* @returns True if the key is valid, false otherwise
|
|
569
|
+
* @throws Error if pi-ai is not installed
|
|
570
|
+
*/
|
|
571
|
+
async validateApiKey(provider: string, apiKey: string): Promise<ApiKeyValidationResult> {
|
|
572
|
+
const piAi = await loadPiAi();
|
|
573
|
+
|
|
574
|
+
// Find the cheapest model for this provider to minimize validation cost
|
|
575
|
+
const cheapestModelId = this.getSmallestModelId(provider);
|
|
576
|
+
if (!cheapestModelId) {
|
|
577
|
+
// No known model, try a generic test with the provider's first model
|
|
578
|
+
const models = piAi.getModels(provider);
|
|
579
|
+
if (models.length === 0) {
|
|
580
|
+
return {
|
|
581
|
+
provider,
|
|
582
|
+
modelId: null,
|
|
583
|
+
valid: false,
|
|
584
|
+
retryable: false,
|
|
585
|
+
status: 'resolution_error',
|
|
586
|
+
message: `No models found for provider "${provider}"`,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const firstRawId = models[0]!['id'];
|
|
590
|
+
const firstRawName = models[0]!['name'];
|
|
591
|
+
const firstModelId = typeof firstRawId === 'string'
|
|
592
|
+
? firstRawId
|
|
593
|
+
: typeof firstRawName === 'string'
|
|
594
|
+
? firstRawName
|
|
595
|
+
: String(firstRawId ?? firstRawName);
|
|
596
|
+
return this.tryValidation(piAi, provider, firstModelId, apiKey);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return this.tryValidation(piAi, provider, cheapestModelId, apiKey);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Check whether a provider's API key is available in environment variables.
|
|
604
|
+
*
|
|
605
|
+
* @param provider - The provider to check
|
|
606
|
+
* @returns The API key if found, null otherwise
|
|
607
|
+
*/
|
|
608
|
+
checkEnvApiKey(provider: string): string | null {
|
|
609
|
+
const entry = PROVIDER_REGISTRY.find(p => p.id === provider);
|
|
610
|
+
if (entry?.envVar) {
|
|
611
|
+
const value = process.env[entry.envVar];
|
|
612
|
+
if (value && value.length > 0) return value;
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// -----------------------------------------------------------------------
|
|
618
|
+
// Model Resolution
|
|
619
|
+
// -----------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Resolve a provider + model ID into a CortexModel.
|
|
623
|
+
*
|
|
624
|
+
* @param provider - The provider identifier
|
|
625
|
+
* @param modelId - The model identifier
|
|
626
|
+
* @returns A CortexModel handle
|
|
627
|
+
* @throws Error if pi-ai is not installed or the model is not found
|
|
628
|
+
*/
|
|
629
|
+
async resolveModel(provider: string, modelId: string): Promise<CortexModel> {
|
|
630
|
+
const piAi = await loadPiAi();
|
|
631
|
+
const piModel = piAi.getModel(provider, modelId);
|
|
632
|
+
let contextWindow: number | undefined;
|
|
633
|
+
if (piModel && typeof piModel === 'object') {
|
|
634
|
+
const raw = piModel as Record<string, unknown>;
|
|
635
|
+
const cw = raw['contextWindow'];
|
|
636
|
+
if (typeof cw === 'number') {
|
|
637
|
+
contextWindow = cw;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return wrapModel(piModel, provider, modelId, contextWindow);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Create a custom model for an OpenAI-compatible endpoint.
|
|
645
|
+
*
|
|
646
|
+
* @param config - Custom model configuration
|
|
647
|
+
* @returns A CortexModel handle
|
|
648
|
+
* @throws Error if pi-ai is not installed
|
|
649
|
+
*/
|
|
650
|
+
async createCustomModel(config: CustomModelConfig): Promise<CortexModel> {
|
|
651
|
+
const piAi = await loadPiAi();
|
|
652
|
+
// Clone an OpenAI model as a base for streaming/format compatibility,
|
|
653
|
+
// then override to use the Chat Completions API. The base model
|
|
654
|
+
// (openai/gpt-4.1) uses the newer Responses API which most
|
|
655
|
+
// OpenAI-compatible endpoints (Ollama, vLLM, etc.) do not support.
|
|
656
|
+
const baseModel = piAi.getModel('openai', 'gpt-4.1');
|
|
657
|
+
const piModel = {
|
|
658
|
+
...(baseModel as Record<string, unknown>),
|
|
659
|
+
id: config.modelId,
|
|
660
|
+
name: config.modelId,
|
|
661
|
+
api: 'openai-completions',
|
|
662
|
+
baseUrl: config.baseUrl,
|
|
663
|
+
provider: 'custom',
|
|
664
|
+
contextWindow: config.contextWindow ?? 128_000,
|
|
665
|
+
// Conservative compat for OpenAI-compatible endpoints: disable
|
|
666
|
+
// features that are OpenAI-specific or may not be supported.
|
|
667
|
+
// Consumer-provided compat overrides are merged on top.
|
|
668
|
+
compat: {
|
|
669
|
+
supportsStore: false,
|
|
670
|
+
supportsDeveloperRole: false,
|
|
671
|
+
supportsStrictMode: false,
|
|
672
|
+
maxTokensField: 'max_tokens' as const,
|
|
673
|
+
...config.compat,
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
// Set API key, using a placeholder for keyless endpoints (e.g., Ollama).
|
|
677
|
+
// The OpenAI SDK client requires a non-empty apiKey value.
|
|
678
|
+
(piModel as Record<string, unknown>)['apiKey'] = config.apiKey ?? 'sk-no-key-required';
|
|
679
|
+
return wrapModel(
|
|
680
|
+
piModel,
|
|
681
|
+
'custom',
|
|
682
|
+
config.modelId,
|
|
683
|
+
config.contextWindow ?? 128_000,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// -----------------------------------------------------------------------
|
|
688
|
+
// Private helpers
|
|
689
|
+
// -----------------------------------------------------------------------
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Get the cheapest known model ID for a provider.
|
|
693
|
+
* Uses the UTILITY_MODEL_DEFAULTS as a proxy for "smallest model."
|
|
694
|
+
*/
|
|
695
|
+
private getSmallestModelId(provider: string): string | null {
|
|
696
|
+
return UTILITY_MODEL_DEFAULTS[provider] ?? null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Attempt to validate an API key by making a minimal LLM call.
|
|
701
|
+
*/
|
|
702
|
+
private async tryValidation(
|
|
703
|
+
piAi: PiAiModule,
|
|
704
|
+
provider: string,
|
|
705
|
+
modelId: string,
|
|
706
|
+
apiKey: string,
|
|
707
|
+
): Promise<ApiKeyValidationResult> {
|
|
708
|
+
try {
|
|
709
|
+
const model = piAi.getModel(provider, modelId);
|
|
710
|
+
|
|
711
|
+
// Try completeSimple first, then complete
|
|
712
|
+
const completeFn = piAi.completeSimple ?? piAi.complete;
|
|
713
|
+
|
|
714
|
+
if (typeof completeFn !== 'function') {
|
|
715
|
+
// Cannot validate without a complete function; assume valid
|
|
716
|
+
// (the consumer will discover failures at first real call)
|
|
717
|
+
return {
|
|
718
|
+
provider,
|
|
719
|
+
modelId,
|
|
720
|
+
valid: true,
|
|
721
|
+
retryable: false,
|
|
722
|
+
status: 'valid',
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const result = await completeFn(
|
|
727
|
+
model,
|
|
728
|
+
{ messages: [{ role: 'user', content: 'hi' }] },
|
|
729
|
+
{ apiKey, maxTokens: 1 },
|
|
730
|
+
);
|
|
731
|
+
const silentError = this.extractSilentValidationError(result);
|
|
732
|
+
if (silentError) {
|
|
733
|
+
throw new Error(silentError);
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
provider,
|
|
737
|
+
modelId,
|
|
738
|
+
valid: true,
|
|
739
|
+
retryable: false,
|
|
740
|
+
status: 'valid',
|
|
741
|
+
};
|
|
742
|
+
} catch (err) {
|
|
743
|
+
return this.classifyValidationError(provider, modelId, err);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private classifyValidationError(
|
|
748
|
+
provider: string,
|
|
749
|
+
modelId: string,
|
|
750
|
+
err: unknown,
|
|
751
|
+
): ApiKeyValidationResult {
|
|
752
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
753
|
+
const normalized = message.toLowerCase();
|
|
754
|
+
|
|
755
|
+
if (
|
|
756
|
+
/\b401\b/.test(normalized) ||
|
|
757
|
+
/\b403\b/.test(normalized) ||
|
|
758
|
+
normalized.includes('invalid api key') ||
|
|
759
|
+
normalized.includes('incorrect api key') ||
|
|
760
|
+
normalized.includes('authentication failed') ||
|
|
761
|
+
normalized.includes('invalid_auth') ||
|
|
762
|
+
normalized.includes('unauthorized') ||
|
|
763
|
+
normalized.includes('forbidden') ||
|
|
764
|
+
normalized.includes('invalid credential')
|
|
765
|
+
) {
|
|
766
|
+
return {
|
|
767
|
+
provider,
|
|
768
|
+
modelId,
|
|
769
|
+
valid: false,
|
|
770
|
+
retryable: false,
|
|
771
|
+
status: 'invalid_credentials',
|
|
772
|
+
message,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (
|
|
777
|
+
/\b429\b/.test(normalized) ||
|
|
778
|
+
/\b500\b/.test(normalized) ||
|
|
779
|
+
/\b502\b/.test(normalized) ||
|
|
780
|
+
/\b503\b/.test(normalized) ||
|
|
781
|
+
/\b504\b/.test(normalized) ||
|
|
782
|
+
normalized.includes('rate limit') ||
|
|
783
|
+
normalized.includes('timeout') ||
|
|
784
|
+
normalized.includes('timed out') ||
|
|
785
|
+
normalized.includes('temporar') ||
|
|
786
|
+
normalized.includes('overloaded') ||
|
|
787
|
+
normalized.includes('unavailable') ||
|
|
788
|
+
normalized.includes('server error') ||
|
|
789
|
+
normalized.includes('network') ||
|
|
790
|
+
normalized.includes('econn') ||
|
|
791
|
+
normalized.includes('enotfound') ||
|
|
792
|
+
normalized.includes('eai_again')
|
|
793
|
+
) {
|
|
794
|
+
return {
|
|
795
|
+
provider,
|
|
796
|
+
modelId,
|
|
797
|
+
valid: false,
|
|
798
|
+
retryable: true,
|
|
799
|
+
status: 'transient_error',
|
|
800
|
+
message,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
provider,
|
|
806
|
+
modelId,
|
|
807
|
+
valid: false,
|
|
808
|
+
retryable: false,
|
|
809
|
+
status: 'resolution_error',
|
|
810
|
+
message,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private extractSilentValidationError(result: unknown): string | null {
|
|
815
|
+
if (!result || typeof result !== 'object') return null;
|
|
816
|
+
const msg = result as Record<string, unknown>;
|
|
817
|
+
if (msg['stopReason'] !== 'error') return null;
|
|
818
|
+
const errorMessage = msg['errorMessage'];
|
|
819
|
+
return typeof errorMessage === 'string'
|
|
820
|
+
? errorMessage
|
|
821
|
+
: 'Provider validation failed';
|
|
822
|
+
}
|
|
823
|
+
}
|