@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.
- package/README.md +55 -10
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/modelSelection.js +53 -15
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +75 -18
- package/dist/src/browser/actions/thinkingTime.js +23 -8
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +41 -7
- 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/sessionRunner.js +72 -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 +39 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +228 -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 +281 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +157 -54
- 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 +5 -1
- package/package.json +3 -1
package/dist/src/mcp/types.js
CHANGED
|
@@ -11,6 +11,7 @@ export const consultInputSchema = z
|
|
|
11
11
|
browserModelLabel: z.string().optional(),
|
|
12
12
|
browserAttachments: z.enum(["auto", "never", "always"]).optional(),
|
|
13
13
|
browserBundleFiles: z.boolean().optional(),
|
|
14
|
+
browserBundleFormat: z.enum(["text", "zip"]).optional(),
|
|
14
15
|
browserThinkingTime: z.enum(["light", "standard", "extended", "heavy"]).optional(),
|
|
15
16
|
browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
|
|
16
17
|
browserResearchMode: z.enum(["deep"]).optional(),
|
package/dist/src/mcp/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveRunOptionsFromConfig } from "../cli/runOptions.js";
|
|
2
2
|
import { Launcher } from "chrome-launcher";
|
|
3
|
-
export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserFollowUps, userConfig, env = process.env, }) {
|
|
3
|
+
export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserBundleFormat, browserFollowUps, userConfig, env = process.env, }) {
|
|
4
4
|
// Normalize CLI-style inputs through the shared resolver so config/env defaults apply,
|
|
5
5
|
// then overlay MCP-only overrides such as explicit search toggles.
|
|
6
6
|
const mergedModels = Array.isArray(models) && models.length > 0
|
|
@@ -24,6 +24,9 @@ export function mapConsultToRunOptions({ prompt, files, model, models, engine, s
|
|
|
24
24
|
if (typeof browserBundleFiles === "boolean") {
|
|
25
25
|
result.runOptions.browserBundleFiles = browserBundleFiles;
|
|
26
26
|
}
|
|
27
|
+
if (browserBundleFormat) {
|
|
28
|
+
result.runOptions.browserBundleFormat = browserBundleFormat;
|
|
29
|
+
}
|
|
27
30
|
if (Array.isArray(browserFollowUps)) {
|
|
28
31
|
result.runOptions.browserFollowUps = browserFollowUps
|
|
29
32
|
.map((entry) => entry.trim())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const NATIVE_API_HOSTS = [
|
|
2
|
+
"api.openai.com",
|
|
3
|
+
"api.anthropic.com",
|
|
4
|
+
"generativelanguage.googleapis.com",
|
|
5
|
+
"api.x.ai",
|
|
6
|
+
];
|
|
7
|
+
export function isCustomBaseUrl(baseUrl) {
|
|
8
|
+
if (!baseUrl)
|
|
9
|
+
return false;
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(baseUrl);
|
|
12
|
+
return !NATIVE_API_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -4,28 +4,7 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { createGeminiClient } from "./gemini.js";
|
|
5
5
|
import { createClaudeClient } from "./claude.js";
|
|
6
6
|
import { isOpenRouterBaseUrl } from "./modelResolver.js";
|
|
7
|
-
|
|
8
|
-
* Known native API base URLs that should still use their dedicated SDKs.
|
|
9
|
-
* Any other custom base URL is treated as an OpenAI-compatible proxy and
|
|
10
|
-
* all models are routed through the chat/completions adapter.
|
|
11
|
-
*/
|
|
12
|
-
const NATIVE_API_HOSTS = [
|
|
13
|
-
"api.openai.com",
|
|
14
|
-
"api.anthropic.com",
|
|
15
|
-
"generativelanguage.googleapis.com",
|
|
16
|
-
"api.x.ai",
|
|
17
|
-
];
|
|
18
|
-
export function isCustomBaseUrl(baseUrl) {
|
|
19
|
-
if (!baseUrl)
|
|
20
|
-
return false;
|
|
21
|
-
try {
|
|
22
|
-
const url = new URL(baseUrl);
|
|
23
|
-
return !NATIVE_API_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
7
|
+
import { isCustomBaseUrl } from "./baseUrl.js";
|
|
29
8
|
export function buildAzureResponsesBaseUrl(endpoint) {
|
|
30
9
|
return `${endpoint.replace(/\/+$/, "")}/openai/v1`;
|
|
31
10
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { countTokens as countTokensGpt5Pro } from "gpt-tokenizer/model/gpt-5-pro";
|
|
3
|
-
import { countTokens as countTokensAnthropicRaw } from "@anthropic-ai/tokenizer";
|
|
1
|
+
import { createRequire } from "node:module";
|
|
4
2
|
import { stringifyTokenizerInput } from "./tokenStringifier.js";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
let countTokensGpt5Impl;
|
|
5
|
+
let countTokensGpt5ProImpl;
|
|
6
|
+
let countTokensAnthropicImpl;
|
|
5
7
|
export const DEFAULT_MODEL = "gpt-5.5-pro";
|
|
6
8
|
export const PRO_MODELS = new Set([
|
|
7
9
|
"gpt-5.5-pro",
|
|
@@ -12,7 +14,18 @@ export const PRO_MODELS = new Set([
|
|
|
12
14
|
"claude-4.6-sonnet",
|
|
13
15
|
"claude-4.1-opus",
|
|
14
16
|
]);
|
|
15
|
-
const
|
|
17
|
+
const countTokensGpt5 = (input, options) => {
|
|
18
|
+
countTokensGpt5Impl ??= require("gpt-tokenizer/model/gpt-5").countTokens;
|
|
19
|
+
return countTokensGpt5Impl(input, options);
|
|
20
|
+
};
|
|
21
|
+
const countTokensGpt5Pro = (input, options) => {
|
|
22
|
+
countTokensGpt5ProImpl ??= require("gpt-tokenizer/model/gpt-5-pro").countTokens;
|
|
23
|
+
return countTokensGpt5ProImpl(input, options);
|
|
24
|
+
};
|
|
25
|
+
const countTokensAnthropic = (input) => {
|
|
26
|
+
countTokensAnthropicImpl ??= require("@anthropic-ai/tokenizer").countTokens;
|
|
27
|
+
return countTokensAnthropicImpl(stringifyTokenizerInput(input));
|
|
28
|
+
};
|
|
16
29
|
export const MODEL_CONFIGS = {
|
|
17
30
|
"gpt-5.5-pro": {
|
|
18
31
|
model: "gpt-5.5-pro",
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
import { GoogleGenAI, HarmCategory, HarmBlockThreshold, } from "@google/genai";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
"gemini-3-pro": "gemini-3-pro-preview",
|
|
5
|
-
"gpt-5.5": "gpt-5.5",
|
|
6
|
-
"gpt-5.5-pro": "gpt-5.5-pro",
|
|
7
|
-
"gpt-5.4": "gpt-5.4",
|
|
8
|
-
"gpt-5.4-pro": "gpt-5.4-pro",
|
|
9
|
-
"gpt-5.1-pro": "gpt-5.1-pro",
|
|
10
|
-
"gpt-5-pro": "gpt-5-pro",
|
|
11
|
-
"gpt-5.1": "gpt-5.1",
|
|
12
|
-
"gpt-5.1-codex": "gpt-5.1-codex",
|
|
13
|
-
"gpt-5.2": "gpt-5.2",
|
|
14
|
-
"gpt-5.2-instant": "gpt-5.2-instant",
|
|
15
|
-
"gpt-5.2-pro": "gpt-5.2-pro",
|
|
16
|
-
"claude-4.6-sonnet": "claude-4.6-sonnet",
|
|
17
|
-
"claude-4.1-opus": "claude-4.1-opus",
|
|
18
|
-
"grok-4.1": "grok-4.1",
|
|
19
|
-
};
|
|
20
|
-
export function resolveGeminiModelId(modelName) {
|
|
21
|
-
// Map our logical Gemini names to the exact model ids expected by the SDK.
|
|
22
|
-
return MODEL_ID_MAP[modelName] ?? modelName;
|
|
23
|
-
}
|
|
2
|
+
import { resolveGeminiModelId } from "./geminiModels.js";
|
|
3
|
+
export { resolveGeminiModelId } from "./geminiModels.js";
|
|
24
4
|
export function createGeminiClient(apiKey, modelName = "gemini-3-pro", resolvedModelId) {
|
|
25
5
|
const modelId = resolvedModelId ?? resolveGeminiModelId(modelName);
|
|
26
6
|
const genAI = new GoogleGenAI({ apiKey });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const MODEL_ID_MAP = {
|
|
2
|
+
"gemini-3.1-pro": "gemini-3.1-pro-preview",
|
|
3
|
+
"gemini-3-pro": "gemini-3-pro-preview",
|
|
4
|
+
"gpt-5.5": "gpt-5.5",
|
|
5
|
+
"gpt-5.5-pro": "gpt-5.5-pro",
|
|
6
|
+
"gpt-5.4": "gpt-5.4",
|
|
7
|
+
"gpt-5.4-pro": "gpt-5.4-pro",
|
|
8
|
+
"gpt-5.1-pro": "gpt-5.1-pro",
|
|
9
|
+
"gpt-5-pro": "gpt-5-pro",
|
|
10
|
+
"gpt-5.1": "gpt-5.1",
|
|
11
|
+
"gpt-5.1-codex": "gpt-5.1-codex",
|
|
12
|
+
"gpt-5.2": "gpt-5.2",
|
|
13
|
+
"gpt-5.2-instant": "gpt-5.2-instant",
|
|
14
|
+
"gpt-5.2-pro": "gpt-5.2-pro",
|
|
15
|
+
"claude-4.6-sonnet": "claude-4.6-sonnet",
|
|
16
|
+
"claude-4.1-opus": "claude-4.1-opus",
|
|
17
|
+
"grok-4.1": "grok-4.1",
|
|
18
|
+
};
|
|
19
|
+
export function resolveGeminiModelId(modelName) {
|
|
20
|
+
return MODEL_ID_MAP[modelName] ?? modelName;
|
|
21
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { MODEL_CONFIGS, PRO_MODELS } from "./config.js";
|
|
2
|
-
import { countTokens as countTokensGpt5Pro } from "gpt-tokenizer/model/gpt-5-pro";
|
|
3
3
|
import { pricingFromUsdPerMillion } from "tokentally";
|
|
4
4
|
const OPENROUTER_DEFAULT_BASE = "https://openrouter.ai/api/v1";
|
|
5
5
|
const OPENROUTER_MODELS_ENDPOINT = "https://openrouter.ai/api/v1/models";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
let countTokensGpt5ProImpl;
|
|
8
|
+
const countTokensGpt5Pro = (input, options) => {
|
|
9
|
+
countTokensGpt5ProImpl ??= require("gpt-tokenizer/model/gpt-5-pro").countTokens;
|
|
10
|
+
return countTokensGpt5ProImpl(input, options);
|
|
11
|
+
};
|
|
6
12
|
export function isKnownModel(model) {
|
|
7
13
|
return Object.hasOwn(MODEL_CONFIGS, model);
|
|
8
14
|
}
|
|
@@ -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
|
+
}
|