@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.
- package/README.md +55 -10
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/modelSelection.js +74 -20
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +76 -18
- package/dist/src/browser/actions/thinkingTime.js +133 -19
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +78 -9
- package/dist/src/browser/manualLoginProfile.js +54 -0
- package/dist/src/browser/projectSourcesRunner.js +16 -5
- package/dist/src/browser/prompt.js +56 -37
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattachability.js +22 -0
- package/dist/src/browser/sessionRunner.js +73 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -11
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/docsCheck.js +186 -0
- package/dist/src/cli/engine.js +11 -4
- package/dist/src/cli/options.js +12 -6
- package/dist/src/cli/perfTrace.js +242 -0
- package/dist/src/cli/promptRequirement.js +2 -0
- package/dist/src/cli/providerDoctor.js +85 -0
- package/dist/src/cli/runOptions.js +46 -16
- package/dist/src/cli/sessionDisplay.js +47 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +272 -3
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/duration.js +47 -0
- package/dist/src/mcp/tools/consult.js +19 -3
- package/dist/src/mcp/types.js +1 -0
- package/dist/src/mcp/utils.js +4 -1
- package/dist/src/oracle/baseUrl.js +17 -0
- package/dist/src/oracle/client.js +1 -22
- package/dist/src/oracle/config.js +17 -4
- package/dist/src/oracle/gemini.js +2 -22
- package/dist/src/oracle/geminiModels.js +21 -0
- package/dist/src/oracle/modelResolver.js +7 -1
- package/dist/src/oracle/multiModelRunner.js +20 -2
- package/dist/src/oracle/providerFailures.js +204 -0
- package/dist/src/oracle/providerRoutePlan.js +308 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +104 -107
- package/dist/src/oracle.js +1 -0
- package/dist/src/remote/client.js +8 -0
- package/dist/src/remote/server.js +26 -0
- package/dist/src/sessionManager.js +43 -23
- 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
|
-
:
|
|
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
|
+
}
|