@steipete/oracle 0.11.1 → 0.12.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.
Files changed (47) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/modelSelection.js +53 -15
  4. package/dist/src/browser/actions/navigation.js +5 -3
  5. package/dist/src/browser/actions/promptComposer.js +75 -18
  6. package/dist/src/browser/actions/thinkingTime.js +23 -8
  7. package/dist/src/browser/constants.js +1 -1
  8. package/dist/src/browser/index.js +41 -7
  9. package/dist/src/browser/manualLoginProfile.js +54 -0
  10. package/dist/src/browser/projectSourcesRunner.js +16 -5
  11. package/dist/src/browser/prompt.js +56 -37
  12. package/dist/src/browser/sessionRunner.js +72 -1
  13. package/dist/src/browser/utils.js +1 -47
  14. package/dist/src/browser/zipBundle.js +152 -0
  15. package/dist/src/cli/browserConfig.js +13 -11
  16. package/dist/src/cli/browserDefaults.js +2 -1
  17. package/dist/src/cli/docsCheck.js +186 -0
  18. package/dist/src/cli/engine.js +11 -4
  19. package/dist/src/cli/options.js +12 -6
  20. package/dist/src/cli/perfTrace.js +242 -0
  21. package/dist/src/cli/promptRequirement.js +2 -0
  22. package/dist/src/cli/providerDoctor.js +85 -0
  23. package/dist/src/cli/runOptions.js +46 -16
  24. package/dist/src/cli/sessionDisplay.js +39 -4
  25. package/dist/src/cli/sessionLifecycle.js +38 -0
  26. package/dist/src/cli/sessionRunner.js +228 -3
  27. package/dist/src/cli/sessionTable.js +2 -1
  28. package/dist/src/duration.js +47 -0
  29. package/dist/src/mcp/tools/consult.js +19 -3
  30. package/dist/src/mcp/types.js +1 -0
  31. package/dist/src/mcp/utils.js +4 -1
  32. package/dist/src/oracle/baseUrl.js +17 -0
  33. package/dist/src/oracle/client.js +1 -22
  34. package/dist/src/oracle/config.js +17 -4
  35. package/dist/src/oracle/gemini.js +2 -22
  36. package/dist/src/oracle/geminiModels.js +21 -0
  37. package/dist/src/oracle/modelResolver.js +7 -1
  38. package/dist/src/oracle/multiModelRunner.js +20 -2
  39. package/dist/src/oracle/providerFailures.js +204 -0
  40. package/dist/src/oracle/providerRoutePlan.js +281 -0
  41. package/dist/src/oracle/providerRouting.js +92 -0
  42. package/dist/src/oracle/run.js +157 -54
  43. package/dist/src/oracle.js +1 -0
  44. package/dist/src/remote/client.js +8 -0
  45. package/dist/src/remote/server.js +26 -0
  46. package/dist/src/sessionManager.js +5 -1
  47. package/package.json +3 -1
@@ -0,0 +1,281 @@
1
+ import { isCustomBaseUrl } from "./baseUrl.js";
2
+ import { formatBaseUrlForLog, maskApiKey } from "./logging.js";
3
+ import { defaultOpenRouterBaseUrl, isOpenRouterBaseUrl, normalizeOpenRouterBaseUrl, } from "./modelResolver.js";
4
+ import { resolveProviderRoutingState, validateProviderRouting } from "./providerRouting.js";
5
+ const DEFAULT_PROVIDER_HOSTS = {
6
+ anthropic: "api.anthropic.com",
7
+ google: "generativelanguage.googleapis.com",
8
+ openai: "api.openai.com",
9
+ xai: "api.x.ai",
10
+ };
11
+ export function buildProviderRoutePlan(input) {
12
+ const env = input.env ?? process.env;
13
+ const providerMode = input.providerMode ?? "auto";
14
+ const azureConfigured = Boolean(input.azure?.endpoint?.trim());
15
+ try {
16
+ validateProviderRouting({
17
+ model: input.model,
18
+ providerMode,
19
+ azure: input.azure,
20
+ });
21
+ }
22
+ catch (error) {
23
+ const state = tryResolveProviderRoutingState({
24
+ model: input.model,
25
+ providerMode,
26
+ azure: input.azure,
27
+ });
28
+ const provider = state?.provider ??
29
+ (providerMode === "openai" ? "openai" : inferProviderFromModel(input.model));
30
+ const isAzureOpenAI = state?.isAzureOpenAI ?? providerMode === "azure";
31
+ const key = getKeyForRoute({
32
+ model: input.model,
33
+ provider,
34
+ providerMode,
35
+ isAzureOpenAI,
36
+ baseUrl: input.baseUrl,
37
+ openRouterFallback: false,
38
+ apiKey: input.apiKey,
39
+ env,
40
+ });
41
+ return {
42
+ model: input.model,
43
+ ok: false,
44
+ provider: isAzureOpenAI ? "azure" : provider,
45
+ providerLabel: isAzureOpenAI ? "Azure OpenAI" : providerLabel(provider),
46
+ base: isAzureOpenAI
47
+ ? formatRouteTargetForLog(state?.azureEndpoint ?? input.azure?.endpoint)
48
+ : formatRouteTargetForLog(input.baseUrl, DEFAULT_PROVIDER_HOSTS[provider]),
49
+ keySource: key.source,
50
+ keyPreview: key.preview,
51
+ keyPresent: key.present,
52
+ isAzureOpenAI,
53
+ azureConfigured,
54
+ azureDeploymentName: state?.azureDeploymentName,
55
+ azureNote: azureNote(providerMode, azureConfigured, isAzureOpenAI),
56
+ error: error instanceof Error ? error.message : String(error),
57
+ };
58
+ }
59
+ const state = resolveProviderRoutingState({
60
+ model: input.model,
61
+ providerMode,
62
+ azure: input.azure,
63
+ });
64
+ const provider = state.provider;
65
+ const isAzureOpenAI = state.isAzureOpenAI;
66
+ let baseUrl = input.baseUrl?.trim();
67
+ const providerQualifiedOpenRouterCandidate = !isAzureOpenAI && providerMode !== "openai" && input.model.includes("/");
68
+ if (baseUrl &&
69
+ providerQualifiedOpenRouterCandidate &&
70
+ !isOpenRouterBaseUrl(baseUrl) &&
71
+ !isCustomBaseUrl(baseUrl)) {
72
+ baseUrl = undefined;
73
+ }
74
+ if (!baseUrl) {
75
+ let envBaseUrl;
76
+ if (input.model.startsWith("grok")) {
77
+ envBaseUrl = env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";
78
+ }
79
+ else if (provider === "anthropic") {
80
+ envBaseUrl = env.ANTHROPIC_BASE_URL?.trim();
81
+ }
82
+ else {
83
+ envBaseUrl = env.OPENAI_BASE_URL?.trim();
84
+ }
85
+ if (!providerQualifiedOpenRouterCandidate || (envBaseUrl && isCustomBaseUrl(envBaseUrl))) {
86
+ baseUrl = envBaseUrl;
87
+ }
88
+ }
89
+ const nativeKey = getNativeKey({
90
+ model: input.model,
91
+ provider,
92
+ providerMode,
93
+ isAzureOpenAI,
94
+ apiKey: input.apiKey,
95
+ env,
96
+ });
97
+ const providerQualifiedOpenRouterRoute = providerQualifiedOpenRouterCandidate && !baseUrl;
98
+ const providerKeyMissing = !isAzureOpenAI &&
99
+ (providerQualifiedOpenRouterRoute
100
+ ? true
101
+ : providerMode === "openai"
102
+ ? !nativeKey.present
103
+ : (provider === "openai" && !nativeKey.present) ||
104
+ (provider === "anthropic" && !nativeKey.present) ||
105
+ (provider === "google" && !nativeKey.present) ||
106
+ (provider === "xai" && !nativeKey.present) ||
107
+ provider === "other");
108
+ const openRouterKey = readKey(["OPENROUTER_API_KEY"], env);
109
+ const openRouterFallback = !baseUrl &&
110
+ (providerQualifiedOpenRouterRoute ||
111
+ (providerMode !== "openai" &&
112
+ providerKeyMissing &&
113
+ (provider === "other" || openRouterKey.present)));
114
+ if (openRouterFallback) {
115
+ baseUrl = defaultOpenRouterBaseUrl();
116
+ }
117
+ if (baseUrl && isOpenRouterBaseUrl(baseUrl)) {
118
+ baseUrl = normalizeOpenRouterBaseUrl(baseUrl);
119
+ }
120
+ const key = getKeyForRoute({
121
+ model: input.model,
122
+ provider,
123
+ providerMode,
124
+ isAzureOpenAI,
125
+ baseUrl,
126
+ openRouterFallback,
127
+ apiKey: input.apiKey,
128
+ env,
129
+ });
130
+ const routeProvider = routeProviderLabel({
131
+ provider,
132
+ baseUrl,
133
+ openRouterFallback,
134
+ isAzureOpenAI,
135
+ });
136
+ const fallbackHost = DEFAULT_PROVIDER_HOSTS[provider] ?? DEFAULT_PROVIDER_HOSTS.openai;
137
+ return {
138
+ model: input.model,
139
+ ok: key.present,
140
+ provider: isAzureOpenAI ? "azure" : provider,
141
+ providerLabel: routeProvider,
142
+ base: isAzureOpenAI
143
+ ? formatRouteTargetForLog(state.azureEndpoint)
144
+ : formatRouteTargetForLog(baseUrl, fallbackHost),
145
+ keySource: key.source,
146
+ keyPreview: key.preview,
147
+ keyPresent: key.present,
148
+ isAzureOpenAI,
149
+ azureConfigured,
150
+ azureDeploymentName: state.azureDeploymentName,
151
+ azureNote: azureNote(providerMode, azureConfigured, isAzureOpenAI),
152
+ error: key.present ? undefined : `Missing ${key.source}.`,
153
+ };
154
+ }
155
+ function getNativeKey({ model, provider, providerMode, isAzureOpenAI, apiKey, env, }) {
156
+ return getKeyForRoute({
157
+ model,
158
+ provider,
159
+ providerMode,
160
+ isAzureOpenAI,
161
+ baseUrl: undefined,
162
+ openRouterFallback: false,
163
+ apiKey,
164
+ env,
165
+ });
166
+ }
167
+ function getKeyForRoute({ model, provider, providerMode, isAzureOpenAI, baseUrl, openRouterFallback, apiKey, env, }) {
168
+ if (apiKey) {
169
+ return { source: "apiKey option", preview: maskApiKey(apiKey) ?? "set", present: true };
170
+ }
171
+ if (isAzureOpenAI) {
172
+ return readKey(["AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"], env);
173
+ }
174
+ if (providerMode === "openai") {
175
+ return readKey(["OPENAI_API_KEY"], env);
176
+ }
177
+ if (isOpenRouterBaseUrl(baseUrl) || openRouterFallback) {
178
+ return readKey(["OPENROUTER_API_KEY"], env);
179
+ }
180
+ if (model.includes("/")) {
181
+ return readKey(["OPENROUTER_API_KEY"], env);
182
+ }
183
+ if (model.startsWith("gpt")) {
184
+ return readKey(["OPENAI_API_KEY"], env);
185
+ }
186
+ if (model.startsWith("gemini")) {
187
+ return readKey(["GEMINI_API_KEY"], env);
188
+ }
189
+ if (model.startsWith("claude")) {
190
+ return readKey(["ANTHROPIC_API_KEY"], env);
191
+ }
192
+ if (model.startsWith("grok")) {
193
+ return readKey(["XAI_API_KEY"], env);
194
+ }
195
+ if (provider === "other") {
196
+ return readKey(["OPENROUTER_API_KEY"], env);
197
+ }
198
+ return readKey(["OPENAI_API_KEY"], env);
199
+ }
200
+ function readKey(names, env) {
201
+ for (const name of names) {
202
+ const value = env[name]?.trim();
203
+ if (value) {
204
+ return { source: name, preview: `${name}=${maskApiKey(value) ?? "set"}`, present: true };
205
+ }
206
+ }
207
+ return { source: names.join("|"), preview: "missing", present: false };
208
+ }
209
+ function routeProviderLabel({ provider, baseUrl, openRouterFallback, isAzureOpenAI, }) {
210
+ if (isAzureOpenAI)
211
+ return "Azure OpenAI";
212
+ if (isOpenRouterBaseUrl(baseUrl) || openRouterFallback)
213
+ return "OpenRouter";
214
+ if (baseUrl && isCustomBaseUrl(baseUrl))
215
+ return "OpenAI-compatible";
216
+ return providerLabel(provider);
217
+ }
218
+ function providerLabel(provider) {
219
+ if (provider === "anthropic")
220
+ return "Anthropic";
221
+ if (provider === "google")
222
+ return "Google Gemini";
223
+ if (provider === "xai")
224
+ return "xAI";
225
+ return "OpenAI";
226
+ }
227
+ function tryResolveProviderRoutingState(input) {
228
+ try {
229
+ return resolveProviderRoutingState(input);
230
+ }
231
+ catch {
232
+ return undefined;
233
+ }
234
+ }
235
+ function inferProviderFromModel(model) {
236
+ const prefix = model.includes("/") ? model.split("/", 1)[0] : undefined;
237
+ if (prefix === "openai")
238
+ return "openai";
239
+ if (prefix === "anthropic")
240
+ return "anthropic";
241
+ if (prefix === "google")
242
+ return "google";
243
+ if (prefix === "xai")
244
+ return "xai";
245
+ if (model.startsWith("claude"))
246
+ return "anthropic";
247
+ if (model.startsWith("gemini"))
248
+ return "google";
249
+ if (model.startsWith("grok"))
250
+ return "xai";
251
+ return "other";
252
+ }
253
+ function azureNote(providerMode, azureConfigured, isAzureOpenAI) {
254
+ if (!azureConfigured)
255
+ return undefined;
256
+ if (providerMode === "openai")
257
+ return "ignored, --provider openai/--no-azure is active";
258
+ if (isAzureOpenAI)
259
+ return "active because Azure endpoint is configured";
260
+ return "configured, not used for this model";
261
+ }
262
+ export function formatRouteTargetForLog(raw, fallbackHost = "") {
263
+ if (!raw)
264
+ return fallbackHost;
265
+ try {
266
+ const parsed = new URL(raw);
267
+ const segments = parsed.pathname.split("/").filter(Boolean);
268
+ let routePath = "";
269
+ if (segments.length > 0) {
270
+ routePath = `/${segments[0]}`;
271
+ if (segments.length > 1) {
272
+ routePath += "/...";
273
+ }
274
+ }
275
+ return `${parsed.host}${routePath}`;
276
+ }
277
+ catch {
278
+ const formatted = formatBaseUrlForLog(raw).replace(/^https?:\/\//u, "");
279
+ return formatted || fallbackHost;
280
+ }
281
+ }
@@ -0,0 +1,92 @@
1
+ import { MODEL_CONFIGS } from "./config.js";
2
+ import { PromptValidationError } from "./errors.js";
3
+ import { isKnownModel } from "./modelResolver.js";
4
+ export const AZURE_DEPLOYMENT_REQUIRED_MESSAGE = "Azure mode requires --azure-deployment unless your deployment is literally gpt-5.5-pro. Pass --azure-deployment <deployment> (or set AZURE_OPENAI_DEPLOYMENT), or rerun with --provider openai/--no-azure to use api.openai.com.";
5
+ export function resolveProviderRoutingState({ model, providerMode = "auto", azure, }) {
6
+ const knownModelConfig = isKnownModel(model) ? MODEL_CONFIGS[model] : undefined;
7
+ const provider = knownModelConfig?.provider ?? inferNativeProviderFromModelId(model) ?? "other";
8
+ const azureEndpoint = azure?.endpoint?.trim();
9
+ const azureDeploymentOption = azure?.deployment?.trim();
10
+ const isNonOpenAIModel = provider !== "openai" && provider !== "other";
11
+ const isProviderQualifiedModelId = model.includes("/");
12
+ if (providerMode === "azure" && !azureEndpoint) {
13
+ throw new PromptValidationError("--provider azure requires --azure-endpoint or AZURE_OPENAI_ENDPOINT.", {
14
+ provider: "azure",
15
+ endpoint: "none",
16
+ });
17
+ }
18
+ if (providerMode === "azure" && isNonOpenAIModel) {
19
+ throw new PromptValidationError(`Azure OpenAI provider cannot run ${model}. Choose an OpenAI/Azure deployment model, or rerun without --provider azure for the model's native provider.`, {
20
+ provider: "azure",
21
+ model,
22
+ modelProvider: provider,
23
+ });
24
+ }
25
+ if (providerMode === "openai" && isNonOpenAIModel) {
26
+ throw new PromptValidationError(`OpenAI provider cannot run ${model}. Choose an OpenAI model, or rerun without --provider openai/--no-azure for the model's native provider.`, {
27
+ provider: "openai",
28
+ model,
29
+ modelProvider: provider,
30
+ });
31
+ }
32
+ const isOpenAIFamilyModel = provider === "openai" || model.startsWith("gpt");
33
+ const isCustomAzureModelId = provider === "other" && !model.includes("/");
34
+ const isAzureOpenAI = Boolean(azureEndpoint &&
35
+ providerMode !== "openai" &&
36
+ !isNonOpenAIModel &&
37
+ (providerMode === "azure" ||
38
+ (!isProviderQualifiedModelId &&
39
+ (isOpenAIFamilyModel || Boolean(azureDeploymentOption) || isCustomAzureModelId))));
40
+ const implicitAzureDeploymentName = isAzureOpenAI &&
41
+ !azureDeploymentOption &&
42
+ (knownModelConfig?.apiModel ?? knownModelConfig?.model) === "gpt-5.5-pro"
43
+ ? "gpt-5.5-pro"
44
+ : undefined;
45
+ return {
46
+ knownModelConfig,
47
+ provider,
48
+ providerMode,
49
+ azureEndpoint,
50
+ azureDeploymentOption,
51
+ isNonOpenAIModel,
52
+ isAzureOpenAI,
53
+ azureDeploymentName: azureDeploymentOption ?? implicitAzureDeploymentName,
54
+ };
55
+ }
56
+ function inferNativeProviderFromModelId(model) {
57
+ const providerPrefix = model.includes("/") ? model.split("/", 1)[0] : undefined;
58
+ if (providerPrefix === "openai")
59
+ return "openai";
60
+ if (providerPrefix === "anthropic")
61
+ return "anthropic";
62
+ if (providerPrefix === "google")
63
+ return "google";
64
+ if (providerPrefix === "xai")
65
+ return "xai";
66
+ if (model.startsWith("claude"))
67
+ return "anthropic";
68
+ if (model.startsWith("gemini"))
69
+ return "google";
70
+ if (model.startsWith("grok"))
71
+ return "xai";
72
+ return undefined;
73
+ }
74
+ export function isAzureOpenAICandidateModel(model) {
75
+ const knownModelConfig = isKnownModel(model) ? MODEL_CONFIGS[model] : undefined;
76
+ const provider = knownModelConfig?.provider ?? inferNativeProviderFromModelId(model) ?? "other";
77
+ return (provider === "openai" ||
78
+ model.startsWith("gpt") ||
79
+ (provider === "other" && !model.includes("/")));
80
+ }
81
+ export function validateProviderRouting(input, hooks = {}) {
82
+ const state = resolveProviderRoutingState(input);
83
+ if (state.isAzureOpenAI && !state.azureDeploymentName) {
84
+ hooks.onAzureDeploymentMissing?.(state);
85
+ throw new PromptValidationError(AZURE_DEPLOYMENT_REQUIRED_MESSAGE, {
86
+ provider: "azure",
87
+ endpoint: state.azureEndpoint ?? "none",
88
+ deployment: "none",
89
+ });
90
+ }
91
+ return state;
92
+ }