@steipete/oracle 0.11.1 → 0.12.1

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 (49) 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 +74 -20
  4. package/dist/src/browser/actions/navigation.js +5 -3
  5. package/dist/src/browser/actions/promptComposer.js +76 -18
  6. package/dist/src/browser/actions/thinkingTime.js +133 -19
  7. package/dist/src/browser/constants.js +1 -1
  8. package/dist/src/browser/index.js +78 -9
  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/providers/chatgptDomProvider.js +1 -0
  13. package/dist/src/browser/reattachability.js +22 -0
  14. package/dist/src/browser/sessionRunner.js +73 -1
  15. package/dist/src/browser/utils.js +1 -47
  16. package/dist/src/browser/zipBundle.js +152 -0
  17. package/dist/src/cli/browserConfig.js +13 -11
  18. package/dist/src/cli/browserDefaults.js +2 -1
  19. package/dist/src/cli/docsCheck.js +186 -0
  20. package/dist/src/cli/engine.js +11 -4
  21. package/dist/src/cli/options.js +12 -6
  22. package/dist/src/cli/perfTrace.js +242 -0
  23. package/dist/src/cli/promptRequirement.js +2 -0
  24. package/dist/src/cli/providerDoctor.js +85 -0
  25. package/dist/src/cli/runOptions.js +46 -16
  26. package/dist/src/cli/sessionDisplay.js +47 -4
  27. package/dist/src/cli/sessionLifecycle.js +38 -0
  28. package/dist/src/cli/sessionRunner.js +272 -3
  29. package/dist/src/cli/sessionTable.js +2 -1
  30. package/dist/src/duration.js +47 -0
  31. package/dist/src/mcp/tools/consult.js +19 -3
  32. package/dist/src/mcp/types.js +1 -0
  33. package/dist/src/mcp/utils.js +4 -1
  34. package/dist/src/oracle/baseUrl.js +17 -0
  35. package/dist/src/oracle/client.js +1 -22
  36. package/dist/src/oracle/config.js +17 -4
  37. package/dist/src/oracle/gemini.js +2 -22
  38. package/dist/src/oracle/geminiModels.js +21 -0
  39. package/dist/src/oracle/modelResolver.js +7 -1
  40. package/dist/src/oracle/multiModelRunner.js +20 -2
  41. package/dist/src/oracle/providerFailures.js +204 -0
  42. package/dist/src/oracle/providerRoutePlan.js +308 -0
  43. package/dist/src/oracle/providerRouting.js +92 -0
  44. package/dist/src/oracle/run.js +104 -107
  45. package/dist/src/oracle.js +1 -0
  46. package/dist/src/remote/client.js +8 -0
  47. package/dist/src/remote/server.js +26 -0
  48. package/dist/src/sessionManager.js +43 -23
  49. package/package.json +15 -12
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from "../oracle.js";
3
+ import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
4
4
  import { sessionStore } from "../sessionStore.js";
5
5
  import { findOscProgressSequences, OSC_PROGRESS_PREFIX } from "osc-progress";
6
6
  function forwardOscProgress(chunk, shouldForward) {
@@ -113,6 +113,13 @@ function startModelExecution({ sessionMeta, runOptions, model, cwd, store, runOr
113
113
  })()
114
114
  .catch(async (error) => {
115
115
  const userError = asOracleUserError(error);
116
+ const providerFailure = classifyProviderFailure(error, {
117
+ model,
118
+ providerMode: runOptions.provider,
119
+ azure: runOptions.azure,
120
+ baseUrl: runOptions.baseUrl,
121
+ apiKey: runOptions.apiKey,
122
+ });
116
123
  const responseMetadata = error instanceof OracleResponseError ? error.metadata : undefined;
117
124
  const transportMetadata = error instanceof OracleTransportError ? { reason: error.reason } : undefined;
118
125
  await store.updateModelRun(sessionMeta.id, model, {
@@ -126,7 +133,18 @@ function startModelExecution({ sessionMeta, runOptions, model, cwd, store, runOr
126
133
  message: userError.message,
127
134
  details: userError.details,
128
135
  }
129
- : undefined,
136
+ : providerFailure
137
+ ? {
138
+ category: providerFailure.category,
139
+ message: providerFailure.label,
140
+ details: {
141
+ provider: providerFailure.provider,
142
+ keyEnv: providerFailure.keyEnv,
143
+ providerMessage: providerFailure.providerMessage,
144
+ fix: providerFailure.fix,
145
+ },
146
+ }
147
+ : undefined,
130
148
  log: await describeLog(sessionMeta.id, logWriter.logPath, store),
131
149
  });
132
150
  throw error;
@@ -0,0 +1,204 @@
1
+ import { OracleTransportError, asOracleUserError } from "./errors.js";
2
+ import { buildProviderRoutePlan } from "./providerRoutePlan.js";
3
+ export function classifyProviderFailure(error, context) {
4
+ const userError = asOracleUserError(error);
5
+ if (userError) {
6
+ return null;
7
+ }
8
+ const normalizedContext = normalizeContext(context);
9
+ const route = inferFailureRoute(normalizedContext);
10
+ const rawProviderMessage = extractProviderMessage(error);
11
+ const lower = rawProviderMessage.toLowerCase();
12
+ const category = classifyMessage(lower, error);
13
+ if (!category) {
14
+ return null;
15
+ }
16
+ const providerMessage = sanitizeProviderMessage(rawProviderMessage);
17
+ return {
18
+ category,
19
+ label: labelForCategory(category),
20
+ provider: route.provider,
21
+ keyEnv: route.keySource,
22
+ providerMessage,
23
+ fix: fixForCategory(category, route.provider, normalizedContext.model, route.keySource),
24
+ };
25
+ }
26
+ function classifyMessage(lower, error) {
27
+ if (isLocalPermissionError(error, lower)) {
28
+ return null;
29
+ }
30
+ if (lower.includes("expired") &&
31
+ (lower.includes("api key") || lower.includes("credential") || lower.includes("token"))) {
32
+ return "auth-expired";
33
+ }
34
+ if (lower.includes("invalid x-api-key") ||
35
+ lower.includes("invalid api key") ||
36
+ lower.includes("api key is invalid") ||
37
+ lower.includes("api key not valid") ||
38
+ lower.includes("incorrect api key") ||
39
+ lower.includes("unauthorized") ||
40
+ lower.includes("unauthenticated") ||
41
+ hasStatusCode(lower, "401")) {
42
+ return "auth-failed";
43
+ }
44
+ if (lower.includes("insufficient_quota") ||
45
+ lower.includes("quota exceeded") ||
46
+ lower.includes("billing") ||
47
+ lower.includes("resource_exhausted")) {
48
+ return "quota-exceeded";
49
+ }
50
+ if (lower.includes("rate limit") || lower.includes("rate_limit") || hasStatusCode(lower, "429")) {
51
+ return "rate-limited";
52
+ }
53
+ if (error instanceof OracleTransportError && error.reason === "model-unavailable") {
54
+ return "model-unavailable";
55
+ }
56
+ if (lower.includes("model not available") ||
57
+ lower.includes("model_not_found") ||
58
+ lower.includes("unknown model") ||
59
+ lower.includes("does not exist")) {
60
+ return "model-unavailable";
61
+ }
62
+ return null;
63
+ }
64
+ function hasStatusCode(lower, status) {
65
+ return new RegExp(`(^|\\D)${status}(\\D|$)`).test(lower);
66
+ }
67
+ function isLocalPermissionError(error, lower) {
68
+ const code = typeof error === "object" && error !== null && "code" in error
69
+ ? String(error.code).toUpperCase()
70
+ : "";
71
+ return (code === "EACCES" ||
72
+ code === "EPERM" ||
73
+ lower.includes("eacces:") ||
74
+ lower.includes("eperm:") ||
75
+ /permission denied, (open|scandir|mkdir|access|unlink|rename)\b/.test(lower));
76
+ }
77
+ function extractProviderMessage(error) {
78
+ return error instanceof Error ? error.message : String(error);
79
+ }
80
+ export function sanitizeProviderMessage(message) {
81
+ return message
82
+ .replace(/\bBearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [redacted]")
83
+ .replace(/\b(api[-_ ]?key\s+provided)(\s*[:=]?\s*)["']?[^"',;\s]+/gi, "$1$2[redacted]")
84
+ .replace(/\b(api[-_ ]?key|x-api-key|authorization|token)(\s*[:=]\s*)["']?[^"',;\s]+/gi, "$1$2[redacted]")
85
+ .replace(/\bsk-(?:ant-|or-)?[A-Za-z0-9_-]{8,}\b/g, "sk-...[redacted]")
86
+ .replace(/\bxai-[A-Za-z0-9_-]{8,}\b/g, "xai-...[redacted]")
87
+ .replace(/\bAIza[0-9A-Za-z_-]{8,}\b/g, "AIza...[redacted]");
88
+ }
89
+ function normalizeContext(context) {
90
+ if (typeof context === "string") {
91
+ return { model: context };
92
+ }
93
+ return context ?? {};
94
+ }
95
+ function inferFailureRoute(context) {
96
+ if (context.model) {
97
+ const plan = buildProviderRoutePlan({
98
+ model: context.model,
99
+ providerMode: context.providerMode,
100
+ azure: context.azure,
101
+ baseUrl: context.baseUrl,
102
+ apiKey: context.apiKey,
103
+ env: context.env,
104
+ });
105
+ const keySource = normalizeKeySource(plan.keySource);
106
+ if (plan.providerLabel === "OpenRouter" || plan.keySource.includes("OPENROUTER_API_KEY")) {
107
+ return { provider: "openrouter", keySource };
108
+ }
109
+ if (plan.provider === "azure")
110
+ return { provider: "azure", keySource };
111
+ if (plan.provider === "google")
112
+ return { provider: "gemini", keySource };
113
+ return { provider: plan.provider, keySource };
114
+ }
115
+ const normalized = context.model?.toLowerCase() ?? "";
116
+ const baseUrl = context.baseUrl?.toLowerCase() ?? "";
117
+ if (baseUrl.includes("openrouter.ai") || (normalized.includes("/") && !baseUrl)) {
118
+ return { provider: "openrouter", keySource: keyEnvForProvider("openrouter") };
119
+ }
120
+ if (context.azure?.endpoint?.trim() &&
121
+ context.providerMode !== "openai" &&
122
+ (normalized.startsWith("gpt") || normalized.startsWith("openai/") || !normalized.includes("/"))) {
123
+ return { provider: "azure", keySource: keyEnvForProvider("azure") };
124
+ }
125
+ if (normalized.startsWith("anthropic/"))
126
+ return providerRoute("anthropic");
127
+ if (normalized.startsWith("google/"))
128
+ return providerRoute("gemini");
129
+ if (normalized.startsWith("xai/"))
130
+ return providerRoute("xai");
131
+ if (normalized.startsWith("openai/"))
132
+ return providerRoute("openai");
133
+ if (normalized.startsWith("claude"))
134
+ return providerRoute("anthropic");
135
+ if (normalized.startsWith("gemini"))
136
+ return providerRoute("gemini");
137
+ if (normalized.startsWith("grok"))
138
+ return providerRoute("xai");
139
+ return providerRoute("openai");
140
+ }
141
+ function providerRoute(provider) {
142
+ return { provider, keySource: keyEnvForProvider(provider) };
143
+ }
144
+ function normalizeKeySource(keySource) {
145
+ if (!keySource || keySource.includes("|")) {
146
+ return undefined;
147
+ }
148
+ return keySource;
149
+ }
150
+ function keyEnvForProvider(provider) {
151
+ switch (provider) {
152
+ case "anthropic":
153
+ return "ANTHROPIC_API_KEY";
154
+ case "gemini":
155
+ return "GEMINI_API_KEY";
156
+ case "xai":
157
+ return "XAI_API_KEY";
158
+ case "azure":
159
+ return "AZURE_OPENAI_API_KEY";
160
+ case "openrouter":
161
+ return "OPENROUTER_API_KEY";
162
+ case "openai":
163
+ return "OPENAI_API_KEY";
164
+ default:
165
+ return undefined;
166
+ }
167
+ }
168
+ function doctorCommand(model) {
169
+ return model ? `oracle doctor --providers --models ${model}` : "oracle doctor --providers";
170
+ }
171
+ function labelForCategory(category) {
172
+ switch (category) {
173
+ case "auth-failed":
174
+ return "auth failed";
175
+ case "auth-expired":
176
+ return "auth expired";
177
+ case "quota-exceeded":
178
+ return "quota exceeded";
179
+ case "rate-limited":
180
+ return "rate limited";
181
+ case "model-unavailable":
182
+ return "model unavailable";
183
+ }
184
+ }
185
+ function fixForCategory(category, provider, model, keySource) {
186
+ const doctor = doctorCommand(model);
187
+ const key = keySource ?? keyEnvForProvider(provider) ?? "the provider API key";
188
+ switch (category) {
189
+ case "auth-failed":
190
+ return key === "apiKey option"
191
+ ? `check --api-key value or run \`${doctor}\``
192
+ : `refresh ${key} or run \`${doctor}\``;
193
+ case "auth-expired":
194
+ return key === "apiKey option"
195
+ ? "replace --api-key value, then rerun the failed model"
196
+ : `rotate ${key}, then rerun the failed model`;
197
+ case "quota-exceeded":
198
+ return `check ${provider} billing/quota, then rerun the failed model`;
199
+ case "rate-limited":
200
+ return "retry later or reduce parallel model fan-out";
201
+ case "model-unavailable":
202
+ return `check model access/ID with \`${doctor}\``;
203
+ }
204
+ }
@@ -0,0 +1,308 @@
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 resolveProviderRoute(input) {
12
+ return buildResolvedProviderRoute(input);
13
+ }
14
+ export function buildProviderRoutePlan(input) {
15
+ const { apiKey: _apiKey, baseUrl: _baseUrl, nativeProvider: _nativeProvider, openRouterFallback: _openRouterFallback, azureEndpoint: _azureEndpoint, ...plan } = buildResolvedProviderRoute(input);
16
+ return plan;
17
+ }
18
+ function buildResolvedProviderRoute(input) {
19
+ const env = input.env ?? process.env;
20
+ const providerMode = input.providerMode ?? "auto";
21
+ const azureConfigured = Boolean(input.azure?.endpoint?.trim());
22
+ try {
23
+ validateProviderRouting({
24
+ model: input.model,
25
+ providerMode,
26
+ azure: input.azure,
27
+ });
28
+ }
29
+ catch (error) {
30
+ const state = tryResolveProviderRoutingState({
31
+ model: input.model,
32
+ providerMode,
33
+ azure: input.azure,
34
+ });
35
+ const provider = state?.provider ??
36
+ (providerMode === "openai" ? "openai" : inferProviderFromModel(input.model));
37
+ const isAzureOpenAI = state?.isAzureOpenAI ?? providerMode === "azure";
38
+ const key = getKeyForRoute({
39
+ model: input.model,
40
+ provider,
41
+ providerMode,
42
+ isAzureOpenAI,
43
+ baseUrl: input.baseUrl,
44
+ openRouterFallback: false,
45
+ apiKey: input.apiKey,
46
+ env,
47
+ });
48
+ return {
49
+ model: input.model,
50
+ ok: false,
51
+ provider: isAzureOpenAI ? "azure" : provider,
52
+ providerLabel: isAzureOpenAI ? "Azure OpenAI" : providerLabel(provider),
53
+ base: isAzureOpenAI
54
+ ? formatRouteTargetForLog(state?.azureEndpoint ?? input.azure?.endpoint)
55
+ : formatRouteTargetForLog(input.baseUrl, DEFAULT_PROVIDER_HOSTS[provider]),
56
+ keySource: key.source,
57
+ keyPreview: key.preview,
58
+ keyPresent: key.present,
59
+ apiKey: key.value,
60
+ nativeProvider: provider,
61
+ baseUrl: input.baseUrl,
62
+ openRouterFallback: false,
63
+ isAzureOpenAI,
64
+ azureEndpoint: state?.azureEndpoint ?? input.azure?.endpoint,
65
+ azureConfigured,
66
+ azureDeploymentName: state?.azureDeploymentName,
67
+ azureNote: azureNote(providerMode, azureConfigured, isAzureOpenAI),
68
+ error: error instanceof Error ? error.message : String(error),
69
+ };
70
+ }
71
+ const state = resolveProviderRoutingState({
72
+ model: input.model,
73
+ providerMode,
74
+ azure: input.azure,
75
+ });
76
+ const provider = state.provider;
77
+ const isAzureOpenAI = state.isAzureOpenAI;
78
+ let baseUrl = input.baseUrl?.trim();
79
+ const providerQualifiedOpenRouterCandidate = !isAzureOpenAI && providerMode !== "openai" && input.model.includes("/");
80
+ if (baseUrl &&
81
+ providerQualifiedOpenRouterCandidate &&
82
+ !isOpenRouterBaseUrl(baseUrl) &&
83
+ !isCustomBaseUrl(baseUrl)) {
84
+ baseUrl = undefined;
85
+ }
86
+ if (!baseUrl) {
87
+ let envBaseUrl;
88
+ if (input.model.startsWith("grok")) {
89
+ envBaseUrl = env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";
90
+ }
91
+ else if (provider === "anthropic") {
92
+ envBaseUrl = env.ANTHROPIC_BASE_URL?.trim();
93
+ }
94
+ else {
95
+ envBaseUrl = env.OPENAI_BASE_URL?.trim();
96
+ }
97
+ if (!providerQualifiedOpenRouterCandidate || (envBaseUrl && isCustomBaseUrl(envBaseUrl))) {
98
+ baseUrl = envBaseUrl;
99
+ }
100
+ }
101
+ const nativeKey = getNativeKey({
102
+ model: input.model,
103
+ provider,
104
+ providerMode,
105
+ isAzureOpenAI,
106
+ apiKey: input.apiKey,
107
+ env,
108
+ });
109
+ const providerQualifiedOpenRouterRoute = providerQualifiedOpenRouterCandidate && !baseUrl;
110
+ const providerKeyMissing = !isAzureOpenAI &&
111
+ (providerQualifiedOpenRouterRoute
112
+ ? true
113
+ : providerMode === "openai"
114
+ ? !nativeKey.present
115
+ : (provider === "openai" && !nativeKey.present) ||
116
+ (provider === "anthropic" && !nativeKey.present) ||
117
+ (provider === "google" && !nativeKey.present) ||
118
+ (provider === "xai" && !nativeKey.present) ||
119
+ provider === "other");
120
+ const openRouterKey = readKey(["OPENROUTER_API_KEY"], env);
121
+ const openRouterFallback = !baseUrl &&
122
+ (providerQualifiedOpenRouterRoute ||
123
+ (providerMode !== "openai" &&
124
+ providerKeyMissing &&
125
+ (provider === "other" || openRouterKey.present)));
126
+ if (openRouterFallback) {
127
+ baseUrl = defaultOpenRouterBaseUrl();
128
+ }
129
+ if (baseUrl && isOpenRouterBaseUrl(baseUrl)) {
130
+ baseUrl = normalizeOpenRouterBaseUrl(baseUrl);
131
+ }
132
+ const key = getKeyForRoute({
133
+ model: input.model,
134
+ provider,
135
+ providerMode,
136
+ isAzureOpenAI,
137
+ baseUrl,
138
+ openRouterFallback,
139
+ apiKey: input.apiKey,
140
+ env,
141
+ });
142
+ const routeProvider = routeProviderLabel({
143
+ provider,
144
+ baseUrl,
145
+ openRouterFallback,
146
+ isAzureOpenAI,
147
+ });
148
+ const fallbackHost = DEFAULT_PROVIDER_HOSTS[provider] ?? DEFAULT_PROVIDER_HOSTS.openai;
149
+ return {
150
+ model: input.model,
151
+ ok: key.present,
152
+ provider: isAzureOpenAI ? "azure" : provider,
153
+ providerLabel: routeProvider,
154
+ base: isAzureOpenAI
155
+ ? formatRouteTargetForLog(state.azureEndpoint)
156
+ : formatRouteTargetForLog(baseUrl, fallbackHost),
157
+ keySource: key.source,
158
+ keyPreview: key.preview,
159
+ keyPresent: key.present,
160
+ apiKey: key.value,
161
+ nativeProvider: provider,
162
+ baseUrl,
163
+ openRouterFallback,
164
+ isAzureOpenAI,
165
+ azureEndpoint: state.azureEndpoint,
166
+ azureConfigured,
167
+ azureDeploymentName: state.azureDeploymentName,
168
+ azureNote: azureNote(providerMode, azureConfigured, isAzureOpenAI),
169
+ error: key.present ? undefined : `Missing ${key.source}.`,
170
+ };
171
+ }
172
+ function getNativeKey({ model, provider, providerMode, isAzureOpenAI, apiKey, env, }) {
173
+ return getKeyForRoute({
174
+ model,
175
+ provider,
176
+ providerMode,
177
+ isAzureOpenAI,
178
+ baseUrl: undefined,
179
+ openRouterFallback: false,
180
+ apiKey,
181
+ env,
182
+ });
183
+ }
184
+ function getKeyForRoute({ model, provider, providerMode, isAzureOpenAI, baseUrl, openRouterFallback, apiKey, env, }) {
185
+ if (apiKey) {
186
+ return {
187
+ source: "apiKey option",
188
+ preview: maskApiKey(apiKey) ?? "set",
189
+ present: true,
190
+ value: apiKey,
191
+ };
192
+ }
193
+ if (isAzureOpenAI) {
194
+ return readKey(["AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"], env);
195
+ }
196
+ if (providerMode === "openai") {
197
+ return readKey(["OPENAI_API_KEY"], env);
198
+ }
199
+ if (isOpenRouterBaseUrl(baseUrl) || openRouterFallback) {
200
+ return readKey(["OPENROUTER_API_KEY"], env);
201
+ }
202
+ if (model.includes("/")) {
203
+ return readKey(["OPENROUTER_API_KEY"], env);
204
+ }
205
+ if (model.startsWith("gpt")) {
206
+ return readKey(["OPENAI_API_KEY"], env);
207
+ }
208
+ if (model.startsWith("gemini")) {
209
+ return readKey(["GEMINI_API_KEY"], env);
210
+ }
211
+ if (model.startsWith("claude")) {
212
+ return readKey(["ANTHROPIC_API_KEY"], env);
213
+ }
214
+ if (model.startsWith("grok")) {
215
+ return readKey(["XAI_API_KEY"], env);
216
+ }
217
+ if (provider === "other") {
218
+ return readKey(["OPENROUTER_API_KEY"], env);
219
+ }
220
+ return readKey(["OPENAI_API_KEY"], env);
221
+ }
222
+ function readKey(names, env) {
223
+ for (const name of names) {
224
+ const value = env[name]?.trim();
225
+ if (value) {
226
+ return {
227
+ source: name,
228
+ preview: `${name}=${maskApiKey(value) ?? "set"}`,
229
+ present: true,
230
+ value,
231
+ };
232
+ }
233
+ }
234
+ return { source: names.join("|"), preview: "missing", present: false };
235
+ }
236
+ function routeProviderLabel({ provider, baseUrl, openRouterFallback, isAzureOpenAI, }) {
237
+ if (isAzureOpenAI)
238
+ return "Azure OpenAI";
239
+ if (isOpenRouterBaseUrl(baseUrl) || openRouterFallback)
240
+ return "OpenRouter";
241
+ if (baseUrl && isCustomBaseUrl(baseUrl))
242
+ return "OpenAI-compatible";
243
+ return providerLabel(provider);
244
+ }
245
+ function providerLabel(provider) {
246
+ if (provider === "anthropic")
247
+ return "Anthropic";
248
+ if (provider === "google")
249
+ return "Google Gemini";
250
+ if (provider === "xai")
251
+ return "xAI";
252
+ return "OpenAI";
253
+ }
254
+ function tryResolveProviderRoutingState(input) {
255
+ try {
256
+ return resolveProviderRoutingState(input);
257
+ }
258
+ catch {
259
+ return undefined;
260
+ }
261
+ }
262
+ function inferProviderFromModel(model) {
263
+ const prefix = model.includes("/") ? model.split("/", 1)[0] : undefined;
264
+ if (prefix === "openai")
265
+ return "openai";
266
+ if (prefix === "anthropic")
267
+ return "anthropic";
268
+ if (prefix === "google")
269
+ return "google";
270
+ if (prefix === "xai")
271
+ return "xai";
272
+ if (model.startsWith("claude"))
273
+ return "anthropic";
274
+ if (model.startsWith("gemini"))
275
+ return "google";
276
+ if (model.startsWith("grok"))
277
+ return "xai";
278
+ return "other";
279
+ }
280
+ function azureNote(providerMode, azureConfigured, isAzureOpenAI) {
281
+ if (!azureConfigured)
282
+ return undefined;
283
+ if (providerMode === "openai")
284
+ return "ignored, --provider openai/--no-azure is active";
285
+ if (isAzureOpenAI)
286
+ return "active because Azure endpoint is configured";
287
+ return "configured, not used for this model";
288
+ }
289
+ export function formatRouteTargetForLog(raw, fallbackHost = "") {
290
+ if (!raw)
291
+ return fallbackHost;
292
+ try {
293
+ const parsed = new URL(raw);
294
+ const segments = parsed.pathname.split("/").filter(Boolean);
295
+ let routePath = "";
296
+ if (segments.length > 0) {
297
+ routePath = `/${segments[0]}`;
298
+ if (segments.length > 1) {
299
+ routePath += "/...";
300
+ }
301
+ }
302
+ return `${parsed.host}${routePath}`;
303
+ }
304
+ catch {
305
+ const formatted = formatBaseUrlForLog(raw).replace(/^https?:\/\//u, "");
306
+ return formatted || fallbackHost;
307
+ }
308
+ }
@@ -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
+ }