@almightygpt/core 0.10.1 → 0.11.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/dist/adapters/defaults.d.ts.map +1 -1
- package/dist/adapters/defaults.js +5 -0
- package/dist/adapters/defaults.js.map +1 -1
- package/dist/adapters/factory.d.ts.map +1 -1
- package/dist/adapters/factory.js +11 -1
- package/dist/adapters/factory.js.map +1 -1
- package/dist/adapters/index.d.ts +9 -5
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +9 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/ollama.d.ts +47 -0
- package/dist/adapters/ollama.d.ts.map +1 -0
- package/dist/adapters/ollama.js +124 -0
- package/dist/adapters/ollama.js.map +1 -0
- package/dist/adapters/openrouter.d.ts +50 -0
- package/dist/adapters/openrouter.d.ts.map +1 -0
- package/dist/adapters/openrouter.js +148 -0
- package/dist/adapters/openrouter.js.map +1 -0
- package/dist/auth/__tests__/resolver.test.js +18 -0
- package/dist/auth/__tests__/resolver.test.js.map +1 -1
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/types.js +8 -0
- package/dist/auth/types.js.map +1 -1
- package/dist/auth/validator.d.ts.map +1 -1
- package/dist/auth/validator.js +101 -0
- package/dist/auth/validator.js.map +1 -1
- package/dist/config/schema.d.ts +12 -12
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +8 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/adapters/defaults.ts +5 -0
- package/src/adapters/factory.ts +11 -1
- package/src/adapters/index.ts +9 -5
- package/src/adapters/ollama.ts +157 -0
- package/src/adapters/openrouter.ts +194 -0
- package/src/auth/__tests__/resolver.test.ts +24 -0
- package/src/auth/types.ts +15 -1
- package/src/auth/validator.ts +106 -0
- package/src/config/schema.ts +8 -1
- package/src/index.ts +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter adapter (v0.12.0+).
|
|
3
|
+
*
|
|
4
|
+
* OpenRouter is a meta-provider that fronts 100+ models behind a single
|
|
5
|
+
* OpenAI-compatible API. Adding it unlocks DeepSeek, Mistral, Codestral,
|
|
6
|
+
* Llama (hosted), Grok, Qwen, and many others without us shipping one
|
|
7
|
+
* adapter per provider.
|
|
8
|
+
*
|
|
9
|
+
* Reads OPENROUTER_API_KEY from the environment (resolver: explicit →
|
|
10
|
+
* env → keychain → missing).
|
|
11
|
+
*
|
|
12
|
+
* Default model: deepseek/deepseek-chat — cheap, capable, popular OR
|
|
13
|
+
* pick. Users override via the `model` field in their config's agents
|
|
14
|
+
* map, e.g. `model: "anthropic/claude-haiku-4-5"`.
|
|
15
|
+
*
|
|
16
|
+
* Model naming: `<provider>/<model>` as required by OpenRouter
|
|
17
|
+
* (e.g. `deepseek/deepseek-chat`, `meta-llama/llama-3.3-70b-instruct`).
|
|
18
|
+
*
|
|
19
|
+
* Pricing: passed through from OpenRouter's response. Their `usage`
|
|
20
|
+
* object includes a `cost` field (USD) when available; otherwise we
|
|
21
|
+
* estimate from a small pricing table for popular models.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import OpenAI from "openai";
|
|
25
|
+
import { AdapterError, type Adapter, type AdapterInput, type AdapterOutput } from "./types.js";
|
|
26
|
+
import { resolveApiKey } from "../auth/resolver.js";
|
|
27
|
+
import { DEFAULT_MODELS } from "./defaults.js";
|
|
28
|
+
|
|
29
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fallback pricing for popular OpenRouter models in case the API doesn't
|
|
33
|
+
* return cost. USD per 1M tokens. Match by prefix (startsWith).
|
|
34
|
+
*
|
|
35
|
+
* Source: https://openrouter.ai/models (verify periodically).
|
|
36
|
+
*/
|
|
37
|
+
const PRICING_USD_PER_1M: Record<string, { input: number; output: number }> = {
|
|
38
|
+
"deepseek/deepseek-chat": { input: 0.27, output: 1.1 },
|
|
39
|
+
"deepseek/deepseek-coder": { input: 0.27, output: 1.1 },
|
|
40
|
+
"anthropic/claude-haiku-4-5": { input: 1.0, output: 5.0 },
|
|
41
|
+
"anthropic/claude-sonnet-4-6": { input: 3.0, output: 15.0 },
|
|
42
|
+
"openai/gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
43
|
+
"openai/gpt-4o": { input: 2.5, output: 10.0 },
|
|
44
|
+
"meta-llama/llama-3.3-70b-instruct": { input: 0.59, output: 0.79 },
|
|
45
|
+
"qwen/qwen-2.5-coder-32b-instruct": { input: 0.07, output: 0.16 },
|
|
46
|
+
"mistralai/codestral-2508": { input: 0.3, output: 0.9 },
|
|
47
|
+
"x-ai/grok-2": { input: 2.0, output: 10.0 },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const DEFAULT_MODEL = DEFAULT_MODELS.openrouter;
|
|
51
|
+
|
|
52
|
+
export interface OpenRouterAdapterOptions {
|
|
53
|
+
apiKey?: string;
|
|
54
|
+
defaultModel?: string;
|
|
55
|
+
defaultMaxOutputTokens?: number;
|
|
56
|
+
defaultTimeoutMs?: number;
|
|
57
|
+
/** Optional HTTP-Referer for OR's stats / leaderboards. */
|
|
58
|
+
appUrl?: string;
|
|
59
|
+
/** Optional X-Title for OR's stats / leaderboards. */
|
|
60
|
+
appTitle?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class OpenRouterAdapter implements Adapter {
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly provider = "openrouter";
|
|
66
|
+
|
|
67
|
+
private client: OpenAI | null = null;
|
|
68
|
+
private resolved = false;
|
|
69
|
+
private readonly explicitApiKey: string | undefined;
|
|
70
|
+
private readonly appUrl: string | undefined;
|
|
71
|
+
private readonly appTitle: string | undefined;
|
|
72
|
+
readonly defaultModel: string;
|
|
73
|
+
private readonly defaultMaxOutputTokens: number;
|
|
74
|
+
private readonly defaultTimeoutMs: number;
|
|
75
|
+
|
|
76
|
+
constructor(name = "openrouter", options: OpenRouterAdapterOptions = {}) {
|
|
77
|
+
this.name = name;
|
|
78
|
+
this.defaultModel = options.defaultModel ?? DEFAULT_MODEL;
|
|
79
|
+
this.defaultMaxOutputTokens = options.defaultMaxOutputTokens ?? 4096;
|
|
80
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 120_000;
|
|
81
|
+
this.explicitApiKey = options.apiKey;
|
|
82
|
+
this.appUrl = options.appUrl ?? "https://github.com/roxjayanath/almightygpt";
|
|
83
|
+
this.appTitle = options.appTitle ?? "AlmightyGPT";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async ensureClient(): Promise<OpenAI | null> {
|
|
87
|
+
if (this.resolved) return this.client;
|
|
88
|
+
this.resolved = true;
|
|
89
|
+
const opts = this.explicitApiKey ? { explicit: this.explicitApiKey } : {};
|
|
90
|
+
const result = await resolveApiKey("openrouter", opts);
|
|
91
|
+
if (result.source === "missing") return null;
|
|
92
|
+
this.client = new OpenAI({
|
|
93
|
+
apiKey: result.key!,
|
|
94
|
+
baseURL: OPENROUTER_BASE_URL,
|
|
95
|
+
defaultHeaders: {
|
|
96
|
+
...(this.appUrl ? { "HTTP-Referer": this.appUrl } : {}),
|
|
97
|
+
...(this.appTitle ? { "X-Title": this.appTitle } : {}),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
return this.client;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async isAvailable(): Promise<boolean> {
|
|
104
|
+
return (await this.ensureClient()) !== null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async execute(input: AdapterInput): Promise<AdapterOutput> {
|
|
108
|
+
const client = await this.ensureClient();
|
|
109
|
+
if (!client) {
|
|
110
|
+
throw new AdapterError(
|
|
111
|
+
'No OpenRouter API key found. Run "almightygpt auth openrouter" ' +
|
|
112
|
+
"or export OPENROUTER_API_KEY in your environment.",
|
|
113
|
+
this.name,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const model = input.model ?? this.defaultModel;
|
|
118
|
+
const maxOutputTokens = input.maxOutputTokens ?? this.defaultMaxOutputTokens;
|
|
119
|
+
const timeoutMs = input.timeoutMs ?? this.defaultTimeoutMs;
|
|
120
|
+
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
let response: OpenAI.Chat.Completions.ChatCompletion;
|
|
123
|
+
try {
|
|
124
|
+
response = await client.chat.completions.create(
|
|
125
|
+
{
|
|
126
|
+
model,
|
|
127
|
+
max_tokens: maxOutputTokens,
|
|
128
|
+
messages: [
|
|
129
|
+
{ role: "system", content: input.systemPrompt },
|
|
130
|
+
{ role: "user", content: input.userMessage },
|
|
131
|
+
],
|
|
132
|
+
...(input.responseFormat === "json_object"
|
|
133
|
+
? { response_format: { type: "json_object" } }
|
|
134
|
+
: {}),
|
|
135
|
+
},
|
|
136
|
+
{ timeout: timeoutMs },
|
|
137
|
+
);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
throw new AdapterError(
|
|
140
|
+
`OpenRouter call failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
141
|
+
this.name,
|
|
142
|
+
err,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const choice = response.choices[0];
|
|
147
|
+
if (!choice || !choice.message.content) {
|
|
148
|
+
throw new AdapterError(
|
|
149
|
+
"OpenRouter returned an empty response.",
|
|
150
|
+
this.name,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const content = choice.message.content;
|
|
155
|
+
const usage = response.usage as
|
|
156
|
+
| (typeof response.usage & {
|
|
157
|
+
/** OpenRouter returns the actual cost in USD when available. */
|
|
158
|
+
cost?: number;
|
|
159
|
+
})
|
|
160
|
+
| undefined;
|
|
161
|
+
const tokensIn = usage?.prompt_tokens ?? 0;
|
|
162
|
+
const tokensOut = usage?.completion_tokens ?? 0;
|
|
163
|
+
// Prefer OR's reported cost (real, billed); fall back to estimate.
|
|
164
|
+
const costUsd =
|
|
165
|
+
typeof usage?.cost === "number"
|
|
166
|
+
? usage.cost
|
|
167
|
+
: estimateCostUsd(model, tokensIn, tokensOut);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
content,
|
|
171
|
+
tokensIn,
|
|
172
|
+
cachedTokensIn: 0,
|
|
173
|
+
tokensOut,
|
|
174
|
+
costUsd,
|
|
175
|
+
latencyMs: Date.now() - start,
|
|
176
|
+
modelUsed: response.model ?? model,
|
|
177
|
+
provider: this.provider,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function estimateCostUsd(
|
|
183
|
+
model: string,
|
|
184
|
+
tokensIn: number,
|
|
185
|
+
tokensOut: number,
|
|
186
|
+
): number {
|
|
187
|
+
const key = Object.keys(PRICING_USD_PER_1M).find((k) =>
|
|
188
|
+
model.toLowerCase().startsWith(k),
|
|
189
|
+
);
|
|
190
|
+
if (!key) return 0;
|
|
191
|
+
const rates = PRICING_USD_PER_1M[key];
|
|
192
|
+
if (!rates) return 0;
|
|
193
|
+
return (tokensIn / 1_000_000) * rates.input + (tokensOut / 1_000_000) * rates.output;
|
|
194
|
+
}
|
|
@@ -25,6 +25,8 @@ const PROVIDER_ENV_VAR = {
|
|
|
25
25
|
openai: "OPENAI_API_KEY",
|
|
26
26
|
anthropic: "ANTHROPIC_API_KEY",
|
|
27
27
|
google: "GOOGLE_API_KEY",
|
|
28
|
+
openrouter: "OPENROUTER_API_KEY", // added v0.12.0
|
|
29
|
+
// ollama has no env var (local, no auth)
|
|
28
30
|
} as const;
|
|
29
31
|
|
|
30
32
|
function makeKeychainStub(behavior: {
|
|
@@ -168,6 +170,28 @@ describe("resolveApiKey — priority order", () => {
|
|
|
168
170
|
expect(r.key).toBe("primary");
|
|
169
171
|
});
|
|
170
172
|
|
|
173
|
+
it("OpenRouter provider reads OPENROUTER_API_KEY (v0.12.0+)", async () => {
|
|
174
|
+
process.env.OPENROUTER_API_KEY = "from-or";
|
|
175
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
176
|
+
makeKeychainStub({ available: false }),
|
|
177
|
+
);
|
|
178
|
+
const r = await resolveApiKey("openrouter");
|
|
179
|
+
expect(r.source).toBe("env");
|
|
180
|
+
expect(r.key).toBe("from-or");
|
|
181
|
+
expect(r.envVar).toBe("OPENROUTER_API_KEY");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("Ollama has no env vars and falls straight to keychain → missing", async () => {
|
|
185
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
186
|
+
makeKeychainStub({ available: true }),
|
|
187
|
+
);
|
|
188
|
+
// Ollama has no API key — PROVIDER_ENV_VARS["ollama"] is empty.
|
|
189
|
+
// The resolver should skip the env loop entirely and check
|
|
190
|
+
// keychain → return "missing" since the keychain stub is empty.
|
|
191
|
+
const r = await resolveApiKey("ollama");
|
|
192
|
+
expect(r.source).toBe("missing");
|
|
193
|
+
});
|
|
194
|
+
|
|
171
195
|
it("empty env var is treated as not set (falls through to keychain)", async () => {
|
|
172
196
|
process.env.OPENAI_API_KEY = "";
|
|
173
197
|
vi.mocked(getKeychain).mockResolvedValue(
|
package/src/auth/types.ts
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* a stale keychain copy. See docs/claude/v0.8-auth-plan.md.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
export type ProviderId =
|
|
20
|
+
export type ProviderId =
|
|
21
|
+
| "openai"
|
|
22
|
+
| "anthropic"
|
|
23
|
+
| "google"
|
|
24
|
+
// v0.12.0+ meta-providers
|
|
25
|
+
| "openrouter"
|
|
26
|
+
| "ollama";
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Where a resolved API key came from.
|
|
@@ -74,6 +80,11 @@ export const PROVIDER_ENV_VARS: Record<ProviderId, readonly string[]> = {
|
|
|
74
80
|
anthropic: ["ANTHROPIC_API_KEY"],
|
|
75
81
|
// Google's SDK historically accepted both names; honor both.
|
|
76
82
|
google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
83
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
84
|
+
// Ollama is local — no key. The empty list signals "no auth"
|
|
85
|
+
// to the resolver (it just falls through to keychain → missing,
|
|
86
|
+
// and the Ollama adapter bypasses the resolver entirely).
|
|
87
|
+
ollama: [],
|
|
77
88
|
};
|
|
78
89
|
|
|
79
90
|
/**
|
|
@@ -84,4 +95,7 @@ export const PROVIDER_KEY_URLS: Record<ProviderId, string> = {
|
|
|
84
95
|
openai: "https://platform.openai.com/api-keys",
|
|
85
96
|
anthropic: "https://console.anthropic.com/settings/keys",
|
|
86
97
|
google: "https://aistudio.google.com/apikey",
|
|
98
|
+
openrouter: "https://openrouter.ai/keys",
|
|
99
|
+
// Ollama is local — no keys page. Linked instead to install docs.
|
|
100
|
+
ollama: "https://ollama.ai/download",
|
|
87
101
|
};
|
package/src/auth/validator.ts
CHANGED
|
@@ -72,6 +72,20 @@ export async function validateKey(
|
|
|
72
72
|
case "google":
|
|
73
73
|
result = await validateGoogle(key);
|
|
74
74
|
break;
|
|
75
|
+
case "openrouter":
|
|
76
|
+
result = await validateOpenRouter(key);
|
|
77
|
+
break;
|
|
78
|
+
case "ollama":
|
|
79
|
+
// Ollama has no key — "validation" means probing the local
|
|
80
|
+
// daemon. The key arg is ignored.
|
|
81
|
+
result = await validateOllama();
|
|
82
|
+
break;
|
|
83
|
+
default: {
|
|
84
|
+
// Exhaustiveness check — TypeScript will error here if a new
|
|
85
|
+
// ProviderId is added without a switch case.
|
|
86
|
+
const _exhaustive: never = provider;
|
|
87
|
+
throw new Error(`Unknown provider: ${String(_exhaustive)}`);
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
result.latencyMs = Date.now() - start;
|
|
77
91
|
return result;
|
|
@@ -187,6 +201,76 @@ async function validateGoogle(key: string): Promise<ValidationResult> {
|
|
|
187
201
|
}
|
|
188
202
|
}
|
|
189
203
|
|
|
204
|
+
async function validateOpenRouter(key: string): Promise<ValidationResult> {
|
|
205
|
+
// OpenRouter is OpenAI-compatible at https://openrouter.ai/api/v1.
|
|
206
|
+
// Same chat-completions shape — point fetch at OR's base URL.
|
|
207
|
+
const model = DEFAULT_MODELS.openrouter;
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
"content-type": "application/json",
|
|
215
|
+
authorization: `Bearer ${key}`,
|
|
216
|
+
"HTTP-Referer": "https://github.com/roxjayanath/almightygpt",
|
|
217
|
+
"X-Title": "AlmightyGPT validation",
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
model,
|
|
221
|
+
messages: [{ role: "user", content: "hi" }],
|
|
222
|
+
max_tokens: 1,
|
|
223
|
+
}),
|
|
224
|
+
signal: controller.signal,
|
|
225
|
+
});
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
const rawBody = await res.text().catch(() => "");
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
statusCode: res.status,
|
|
231
|
+
error: normalizeOpenRouterError(res.status, rawBody, key),
|
|
232
|
+
rawBody,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const data = (await res.json()) as { model?: string };
|
|
236
|
+
return { ok: true, model: data.model ?? model };
|
|
237
|
+
} finally {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function validateOllama(): Promise<ValidationResult> {
|
|
243
|
+
// No key. Probe the local daemon at /api/tags — cheap, no model
|
|
244
|
+
// invocation. If the user has Ollama running, this confirms the
|
|
245
|
+
// adapter can reach it. Doesn't confirm the default model is
|
|
246
|
+
// pulled — that surfaces on the first real call.
|
|
247
|
+
const baseUrl = process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434";
|
|
248
|
+
const probeUrl = baseUrl.replace(/\/v1\/?$/, "") + "/api/tags";
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetch(probeUrl, { signal: controller.signal });
|
|
253
|
+
if (!res.ok) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
statusCode: res.status,
|
|
257
|
+
error: `[${res.status}] Ollama daemon at ${baseUrl} responded with an error.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { ok: true, model: DEFAULT_MODELS.ollama };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
error:
|
|
265
|
+
`Ollama daemon unreachable at ${baseUrl}. Is Ollama installed ` +
|
|
266
|
+
`and running? Try \`ollama serve\` or open the Ollama app.`,
|
|
267
|
+
rawBody: err instanceof Error ? err.message : String(err),
|
|
268
|
+
};
|
|
269
|
+
} finally {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
190
274
|
// ─── Error normalization (Codex v0.8 P2 #6) ──────────────────────────
|
|
191
275
|
//
|
|
192
276
|
// Parse known provider JSON error shapes into short, user-safe messages.
|
|
@@ -258,6 +342,28 @@ function redactKey(msg: string, key: string): string {
|
|
|
258
342
|
return msg.split(key).join("<redacted-key>");
|
|
259
343
|
}
|
|
260
344
|
|
|
345
|
+
function normalizeOpenRouterError(
|
|
346
|
+
status: number,
|
|
347
|
+
rawBody: string,
|
|
348
|
+
submittedKey: string,
|
|
349
|
+
): string {
|
|
350
|
+
// OpenRouter forwards provider errors but wraps them; shape is
|
|
351
|
+
// typically { "error": { "message": "...", "code": N } }.
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(rawBody) as {
|
|
354
|
+
error?: { message?: string; code?: number };
|
|
355
|
+
};
|
|
356
|
+
let msg = parsed.error?.message ?? "";
|
|
357
|
+
if (submittedKey && msg.includes(submittedKey)) {
|
|
358
|
+
msg = msg.replace(submittedKey, "<redacted-key>");
|
|
359
|
+
}
|
|
360
|
+
if (msg) return `[${status}] OpenRouter: ${truncate(msg, 200)}`;
|
|
361
|
+
} catch {
|
|
362
|
+
/* fall through */
|
|
363
|
+
}
|
|
364
|
+
return statusOnlyMessage("OpenRouter", status);
|
|
365
|
+
}
|
|
366
|
+
|
|
261
367
|
function statusOnlyMessage(provider: string, status: number): string {
|
|
262
368
|
if (status === 401 || status === 403) {
|
|
263
369
|
return `[${status}] ${provider} rejected the key (unauthorized).`;
|
package/src/config/schema.ts
CHANGED
|
@@ -13,7 +13,14 @@ export const AgentRoleSchema = z.enum(["worker", "reviewer", "both", "optional"]
|
|
|
13
13
|
export const AgentConfigSchema = z
|
|
14
14
|
.object({
|
|
15
15
|
enabled: z.boolean().default(true),
|
|
16
|
-
provider: z.enum([
|
|
16
|
+
provider: z.enum([
|
|
17
|
+
"openai",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"google",
|
|
20
|
+
"openrouter",
|
|
21
|
+
"ollama",
|
|
22
|
+
"mock",
|
|
23
|
+
]),
|
|
17
24
|
mode: z.enum(["api", "cli"]).default("api"),
|
|
18
25
|
role: AgentRoleSchema.default("optional"),
|
|
19
26
|
memoryFile: z.string().min(1),
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - budget/ ✅ task #14 BudgetTracker + BudgetExceededError
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
export const VERSION = "0.
|
|
16
|
+
export const VERSION = "0.11.0";
|
|
17
17
|
|
|
18
18
|
// MCP server (v0.9.0+) — exposes AlmightyGPT's review surface as MCP tools.
|
|
19
19
|
export { startMcpServer } from "./mcp/server.js";
|